第四章 类和接口
13、 使类和成员的可访问性最小化
要区别设计良好的模块与设计不好的模块,最后重要的因素在于,这个模块对于外部的其他模块而言,是否隐藏其内部了数据和其他实现细节。设计良好的模块会隐藏所有的实现细节,把它的API与它的实现清晰地隔离开来。然后,模块之间只通过它们的API进行通信,一个模块不要知道其他模块的内部工作情况。这个概念被称为信息隐藏或封装,也是软件设计的基本原则之一。
封装有效地解除组成系统的各模块之间的耦合关系,使得这些模块可以独立地开发、测试与维护,还提高了软件的可重用性。
Java里的封装是通过访问控制符来加以限制的。
第一规则很简单:尽可能地使每个类或者成员不被外界访问,即使用尽可能最小的访问级别。
对于顶层类(非嵌套的)类和接口,只有两种可能的访问级别:包级私有的和公有的。如果使用pulbic修饰符声明了顶层类或者接口,那它就是公有的;否则,它将是包级私有的。如果顶层类或者接口能够被做成包级私有的,它就应该被做成包级私有,这样类或者是接口就成了这个包的实现的一部分,而不是该包导出的API的一部分,在以后的版本中,可以对它进行修改、替换、或者删除,而无需担心会影响到现有的客户端程序。
如果一个包级私有的顶层类(或者接口)只是在某一个类的内部用到,就应该考虑使它成为唯一使用它的那个类的私有嵌套类。这样可以将它的可访问范围从包中的所有类缩小到了使用它的那个类。然而,降低不必要公有类的可访问性,比降低包级私有的顶层类的更重要得多:因为公有类是包的API的一部分,而包级私有的顶层类只是这个包的实现的一部分,包级是我们可以掌控的一部分。
对于成员(域、方法、嵌套类和嵌套接口)有四种可能的访问级别,下面按照可访问性的递增顺序罗列出来:
1、 私有的(private):只有在声明该成员的顶层类内部才可以访问这个成员。
2、 包级私有的:声明该成员的包内部的任何的任何类都可以访问这个成员。这也是“缺省”访问级别,如果没有为成员指定访问修饰符,就采用这个访问级别。
3、 受保护的(protected):声明该成员的类的子类可以访问这个成员(但有一些限制[JLS,6.6.2]),并且声明该成员的包内部的任何类也可以访问这个成员。
4、 公有的(public):在任何地方都可以访问该成员。
只有当同一个包内的另一个类真正需要访问一个成员的时候,你才应该删除private修饰符,使该成员变成包私有的。
私有成员和包级私有成员都是一个类的实现中的一部分,一般不会影响它的导出的API。然而,如果这个类实现了Serializable接口(见第74和75),这些域就有可能会被“泄漏”到导出的API中。
对于公有类的成员,当访问级别从包级私有变成保护级别时,会大大增强可访问性。受保护的成员是类的导出的API的一部分,必须永远得到支持。导出的类的受保护成员也代表了该类对于某个实现细节的公开承诺(见第17条)。受保护的成员应该尽量少用。
如果方法覆盖了超类中的一个方法,子类中的访问级别就不允许低于超类中的访问级别。这样就可以确保任何可使用超类的实例的地方也都可以使用子类的实例。
如果一个类实现了一个接口,那么接口中所有的类方法在这个类中也都必须被声明为公有的。之所以如此,是因为接口中的所有方法都隐含着公有访问级别。
为了测试而将一个公有类的私有成员变成包级别私有的,这还可以接受,但是如果要将访问级别提高到超过包访问级别,这就无法接受了。换包话说,不能为了测试,而将类、接口、或成员变成包的导出的API的一部分。幸运的是,也没有必要这么做,因为可以让测试作为被测试的包的一部分来运行,从而能够访问它的包级私有的元素。
实例域决不能是公有的(见第14条)。如果域是非final的,或者即是final但指向的却是可变对象,那么一旦使这个域成为公有的,就放弃了对存储在这个域中的值进行限制的能力;这意味着,你也放弃了强制这个域不可变的能力。因此,包含公有可变域的类并不是线程安全的。即使域是final的,并且引用不可变的对象,当把这个域变成公有的时候,也就放弃了“切换换到一种新的内部了数据表示法”(比如将这个字段删除掉,或者使用多个字段来表示等)的灵活性。
public final修饰的域要么包含基本类型的值,要么包含指向不可变对象的引用(见第15条)(同样的建议也适用于静态域)。如果final域包含可变对象的引用,它更具有非final域的所有缺点,虽然引用本身不能被修改,但是它所引用的对象却可以被修改。
长度非零的数组总是可变的,所以,类具有公有的静态final数组域,或者返回这种域的访问方法(这与方法返回的是局部数组是不一样的,因为这个是共享的,而返回的局部数组是单个线程共享的),这几乎总是错误的。如果类具有这样的域或者访问方法,客户端将能够修改数组中的内容,这是安全漏洞的一个常见根源:
public static final Thing[] VALUES={…};
修正这个问题有两种方法,可以使公有数组变成私有的,并增加一个公有的不可变列表:
private static final Thing[] PRIVATE_VALUES={…};
public static final LIST VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
另一种方法是,可以使用数组变成私有的,并添加一个公有方法,它返回私有的数组的一个备份:
private static final Thing[]PRIVATE_VALUES ={…};
public static final Thing[] values(){
return PRIVATE_VALUES.clone();
}
但依我个人看,上面的克隆还是不起根本作用(如果数组元素是基本类型的,就没有问题):虽然各个元素引用已被复制,但是它们所引用的对象却是同一份,如果指向的对象是可变的,则还是可以通过引用去修改它们所指向对象的问题,所以上面的修改也有潜在的问题。
从上面分析来看public static final最好修改的是基本类型的变量,或者是不可变的类。
总而言之,你应该始终尽量可能地降低可访问性。除了公有静态final域的特殊情形之外,公有类都不应该包含公有域。并且要确保公有静态final域所引用的对象都是不可变的。
14、 在公有类中使用访问方法而非公有域
如果类可以在它所在的包的外部进行访问,就将域设为私有的并提供域的访问与设置公有方法,以保留将来改变该类的内部表示法的灵活性。如果公有类暴露了它的数据域,要想在将来改变其内部表示法是不可能的,因为公有类的客户端已经遍布各处了。另外,如果不采取这种方式,则当域被访问的时候,无法彩任何辅助的行动。
如果类是包级私有的(虽然将域暴露了,但也只仅限于包含该类的包中,还在你可控的范围之内),或者是私有的嵌套类,直接暴露它的数据域并没有本质的错误——假设这些数据域确实描述了该类所提供的抽象。比如LinkedList中的Entry静态内部类,就暴露了所有的成员给外类了,这样用起来更简洁方便:
public class LinkedList{
//...
private static class Entry {
Object element;
Entry next;
Entry previous;
//...
}
}
让公有类直接暴露域虽然从来都不是种好办法,但是如果域是不可变的,这种做法危害就比较小一些(只是不能改变其指向,但指向的内容是否安全就不好说了)。如果不改变类的API,就无法改变这种类的表示法,当域被读取的时候,你也无法采取辅助的行动。
总之,公有类永远不应该暴露可变的域。虽然还是有问题,但是让公有类暴露不可变域其危害比较小。但是,有时候会需要使包级私有的或者私有的嵌套灰来暴露域,无论这个类是可变还是不可变的。
15、 使可变性最小化
不可变类是其实例不能被修改的类(不只是类前加上了final就可以了)。每个实例中包含的所有信息都必须在创建该实例时候就提供,并在对象的整个生命周期内固定不变。
Java平台类库上有很多不可变的类,其中有String、基本类型的包装类、BigInteger和BigDecimal。
存在不可变内的许多理由:不可变类比可变类更加易于设计、实现和使用。它们不容易出错,且更加安全。
为使类成为不可变,要遵循以下5条规则:
1、 不要提供任何会修改对象状态(属性)的方法。
2、 保证类不会被扩展。一般做法是使这个类成为final的,另外作法就是让类所有构造器都变成私有的或者是包级私有的。
3、 使用有的域都是final的(一般是针对非静态变量)。通过这种加上final修饰的强制方式,这可以清楚地表明你的意图:确保该域在使用前得到正确的初始化。而且,如果一个指向新创建的实例的引用在缺乏同步机制(一般不可变对象的访问是不需要同步的,因为状态一旦确定,就不会再更改)的情况下,从一个线程切换另一个线程就必需确保实例的正确状态(比如刚创建完这个实例,但还没来得及将工作内存中的数据存回到主内存中时就会有问题,但如果是加上了final修饰符后,则不会出现使用前final域还会初始化完成的情况,这样一定能保证构造器调用完后final域也会随之初始化完成。虽然以前很早的虚拟机上会出现构造器执行完成后final域未初始化完成的问题,但现已JMM已修复),正如果内存模型中所述那样[JLS 17.5]。
4、 使用所有的域都成为私有的。这样可以防止客户端获得访问被域引用的可变对象的权限,并防止客户端直接修改这些对象。虽然从技术上讲,允许不可变的类具有公有的final域,只要这些域包含基本类型的值或都指向不可变对象的引用,但是不建议这样做,因为这样会使得在以后的版本中无法以再改变内部的表示法。
5、 确保对于任何可变域的互斥访问。如果类具有指向可变对象域,则必须确保该类的客户端无法获得指向这些对象的引用。并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法中返回该对象的引用(即进出都不行)。在构造器、访问方法、readObject方法(见76条)中请使用保护性拷贝技术。
下面是一个不可变复数(具有实部和虚部)类的例子:
//复数
public final class Complex {
private final double re;//实部
private final double im;//虚部
// 私有的,让它无法扩展
private Complex(double re, double im) {
this.re = re;
this.im = im;
}
//静态工厂方法
public static Complex valueOf(double re, double im) {
return new Complex(re, im);
}
public static Complex valueOfPolar(double r, double theta) {
return new Complex(r * Math.cos(theta), r * Math.sin(theta));
}
public static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);
// 可以直接返回基本类型的值
public double realPart() {
return re;
}
public double imaginaryPart() {
return im;
}
//每次加减乘除都返回一个新的对象
public Complex add(Complex c) {
return new Complex(re + c.re, im + c.im);
}
public Complex subtract(Complex c) {
return new Complex(re - c.re, im - c.im);
}
public Complex multiply(Complex c) {
return new Complex(re * c.re - im * c.im, re * c.im + im * c.re);
}
public Complex divide(Complex c) {
double tmp = c.re * c.re + c.im * c.im;
return new Complex((re * c.re + im * c.im) / tmp, (im * c.re - re
* c.im)
/ tmp);
}
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Complex))
return false;
Complex c = (Complex) o;
// 浮点数的比较要使用Double.compare,而不能直接使用==比较
return Double.compare(re, c.re) == 0 && Double.compare(im, c.im) == 0;
}
public int hashCode() {
int result = 17 + hashDouble(re);
result = 31 * result + hashDouble(im);
return result;
}
private int hashDouble(double val) {
long longBits = Double.doubleToLongBits(re);
return (int) (longBits ^ (longBits >>> 32));
}
public String toString() {
return "(" + re + " + " + im + "i)";
}
}
不可变对象只有一种状态,即被创建时的状态。不可变对象本质上是线程安全的,它们不要求同步,可以被自由的共享。不可变类应该充分利用这种优势,鼓励客户端尽可能地重用现有的实例,要做到这一点,一个很简便的办法就是,对于频繁用到的值,为它们提供公有的静态final常量,例如上面的常量:
public static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);
这种方法可以被进一步的扩展,不可变以的类可以提供一些静态工厂,它们把频繁请求主的实例缓存起来,在请求合适的对象时候,就不必要创建新的实例。所有的基本类型的包装类和BigInteger都有这样的静态工厂。使得实例可以共享,从而降低内存与垃圾回收成本。在设计类时,选择用静态工厂代替公有的构造器可以让你以后有缓存的灵活性,而不必影响客户端。
“不可变对象可以被自由地共享”,我们永远也不需要,也不应该为不可变对的类提供clone方法或者拷贝构造器。这一点在Java平台早期的版本中做得并不好,所以String类仍然具有拷贝构造器,但是应该尽量少用它。
不仅可以共享不可变对象,甚至也可以共享它们的内部信息。如BigInteger的negate方法产生一个新的BigInteger,其中数组是一样的,符号则是相反的,它并不需要拷贝数组;新建的BigInteger也指向原始实例中的同一个内部数组。
不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象,创建这种对象的代价可能很高,特别是对于大型对象的情形。
如果你选择让自己的不可变实现Serializable接口,并具它包含一个或者多个指向可变对象的域,就必须提供一个显示的readObject或者readResolve方法,或者使用ObjectOutputStream.writeUnshared和ObjectInputStream.readUnshared方法,否则攻击者或能从不可变的类创建可变的实例。这个话题详细请参见76条。
除非有很好的理由要让类成为可变的类,否则就应该是不可变的。不可变的类优点有很多,唯一缺点是在特定的情况下存在潜在的性能问题。你应该总是使用一些小的值对象(如Complex),成为不可变的。但你也应该认真考虑把一些较大的值对象做成不可变的(String、BigInteger),只有当你确认有性能问题时,才应该为不可变的类提供一个公有的可变配套类(如String的配套类StringBuffer、StringBuilder;还有BigInteger的配套类为BitSet)。
对于有些类而言,基不可变性是不切实际的。如果为不能被做成是不可变的,仍然应该尽可能地限制它的可变性。除非有使人信服的理由要使域变成是非final的,否则要使每个域都是final的。
构造器应该创建完全初始化的对象,并建立起所有的约束关系。不要在构造器或者静态工厂之外再提供公有的初始化方法,除非有使人信服的理由。同样也不应该提供“重新初始化”方法(比如TimerTask类,它是可变的,一旦完成或被取消,就不可能再对它重新调度),与所增加的复杂性相比,通常并没有带来太多的性能。
关于 不可变对象可变的修复问题 请参考:http://www.ibm.com/developerworks/cn/java/j-jtp02244/
总之,新的内存模型中,以前不可变对象与final域的可变问题得到了修复,可以放心的使用了(为了确保String的安全初始化,1.5中String的value[]、offset、count三个字段前都已加上了final修饰符,这样新的Java内存模型会确认final字段的初始化安全)。
16、 组合优先于继承
继承是实现代码重用的有力手段,但它并非是最好的手段。
在包的内部使用继承是非常安全的,在那里,子类和超类的实现都处在同一个程序员的控制之下,即你还可以修改一下超类的东西。对普通的具体类进行跨越包边界的继承,则是非常危险的,因为你一旦发布包之后,你就得要遵循你的接口,而不能轻易的去修改它而影响客户端已有的应用。
与组合复用不同的是,继承打破了封装性。换句话说,子类依赖于其超类中特定的功能的实现细节。超类的实现有可能会随着发行版本的不同而有所变化,如果真的发生了变化,子类可能会遭到破坏,即使它的代码完全没有改变。
下面程序要使用HashSet功能,直接采用了继承:
public class InstrumentedHashSet<E> extends HashSet<E> {
/*
* 用来记录添加了多少个元素,与HashSet的size是不一样的
* HashSet的size会受到删除的影响,而这个只记录添加的元素
* 个数
*/
private int addCount = 0;
public InstrumentedHashSet() {
}
public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override
//因为要记录我已放入的元素,所以重写了父类的方法
public boolean add(E e) {
addCount++;
return super.add(e);//最终还是调用HashSet 的add方法将元素存入
}
/*
* super.addAll是以调用HashSet 的add方法来实现的,而add方法又被子类
* 重写了,所以该类的add方法也会被再次调用,即实质上调用addAll
* 方法的时候,它也会去调用一下add方法,这在我们不调试的情况下
* 是很难发现的,这就是继承所带来的后果:因为我们依赖了父类的实现的细节。
*/
@Override public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
public static void main(String[] args) {
InstrumentedHashSet<String> s = new InstrumentedHashSet<String>();
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
System.out.println(s.getAddCount());
}
}
这个类看起来非常合理,但是它并不能正常工作,上面的子例返回的是6,而不是我们期望的3,为什么?在HashSet的内部,addAll方法是基于它的add方法来实现的,即addAll是循环调用add来实现的(即使HashSet的文档并没有说明这样的细节也是合理的,因为该类就不是为了用来继承的),而这add方法又被我们设计的这个类InstrumentedHashSet重写了,所以会再次调用我们的add方法。
我们只去掉被重写的addAll方法中的“addCount += c.size();”,就可以“修正”这个子类,但我们不能否认这样的实事:HashSet的addAll方法是在它的add方法上实现的,这种“自用性(self-use)”是实现的细节,它不是承诺,所以Sun不能保证在Java平台的所有版本实现中都保持不变,即不能保证随着发行版本的不同而不发生变化(比如说将来HashSetr addAll不是以调用add方法来实现),这样的实现细节是不必承诺了,他们能承诺的也只是公开的接口而已,因此,这样得到的InstrumentedHashSet类将是非常脆弱的。
这里稍微好一点的做法是,在InstrumentedHashSet中重写addAll方法来遍历指定的集合,为每个元素调用一次add方法,这样做可以保证得到正确的结果 ,不管HashSet的addAll方法是否是以调用add方法来实现的,因为HashSet的addAll实现将不会再被调用:
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
Iterator<? extends E> e = c.iterator();
while (e.hasNext()) {
//直接调用自己重写过的add方法,重写addAll后,不再依赖于HashSet的addAll
if (add(e.next()))
modified = true;
}
return modified;
}
以上实现只是把HashSet的addAll方法Copy过来而已。然而,这项技术并没有解决所有的问题,它相当于重新自己实现了超类的方法,这些超类的方法可能是自用的(self-use),也可能不是,如果是则这里的重新实现还有点意义,如果不是,则重新实现则完全是没有必要的,并且可能很容易出错。另外有一点,也许你说管他父类的addAll方法是否是自用的,我重新实现就是了,但是,你想过没有,如果父类中的addAll修改了某个私有的域的状态,那你该如何做?我们是无法在重新实现时修改父类中的那个私有域的状态的,因为我们根本无法访问到,所以,总之这种方案是不可取的,因为有太多的不确定因素。
导致子类脆弱的一个相关的原因就是,它们的超类在后续的版本中可能添加新的方法。假设我们的程序的有这样一个要求:所有被插入到某个集合中的元素都必须满足某个条件(比如上面在放入之前addCount会记录一下)。这样做是可以确保一点:对集合进行子类化,并覆盖所有能够添加元素的方法,以便确保在加入每个元素之前它是满足这个先决条件的。如果在后续的版本中,超类中没有增加能插入元素的新方法,这种做法是安全的。然而,一旦超类增加了这样的新方法,则很可能仅仅由于调用了这个未被覆盖的新的方法,而将“非法的”元素添加到子类的实例中。这不是纯粹的理念问题,在把Hashtable和Vector加入到集合框架中时,就修正了这几个类性质的安全漏洞。
上面的问题都是来源于覆盖动作。如果在继承一个类的时候,仅仅是添加新的方法,而没有覆盖现有的父类中的方法,你可能会认为是安全的,但是不然,比如你现在已继承了某个类,并扩展了父类,并添加了父类中没有方法,但是有可能将来父类也会添加同样签名的方法,但重写的条件不满足时,此时你设计的子类将不能编译,或者即使满足重写条件,这样又会有问题(如父类私有域状态的维护工作)。这样的问题不是不存在的,因为当你在编写子类方法时候,你肯定是不会知道将来父类会增加这样的名称的方法。
幸运的是,有一种办法可以避免前面提到的两个问题(覆盖可能影响父类私有域的状态不正确,部分覆盖又可能影响“自用性(self-use)”),不用继承现有类,而是在新的类中增加一个私有域,它引用现有类的一个实例。这种设计被称做为“组合”,因为现有类变成了新的类的一个组件。新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果。这被称为“转发”,新类中的方法被称为转发方法,这样得到的类将会非常稳固,它不依赖于现有类的实现细节,即使现有的类添加了新的方法,也不会影响新的类。下面我们使用“组合”来修正前面两个问题,注意这个实现分为两部分:类本身和可重用转发类,转发类没有其他方法,全是转发方法:
//功能不变,接口也不变,这叫转发
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;//组合
public ForwardingSet(Set<E> s) { this.s = s; }
//下面全是转发方法
public void clear() { s.clear(); }
public boolean contains(Object o) { return s.contains(o); }
public boolean isEmpty() { return s.isEmpty(); }
public int size() { return s.size(); }
public Iterator<E> iterator() { return s.iterator(); }
public boolean add(E e) { return s.add(e); }
public boolean remove(Object o) { return s.remove(o); }
public boolean containsAll(Collection<?> c)
{ return s.containsAll(c); }
public boolean addAll(Collection<? extends E> c)
{ return s.addAll(c); }
public boolean removeAll(Collection<?> c)
{ return s.removeAll(c); }
public boolean retainAll(Collection<?> c)
{ return s.retainAll(c); }
public Object[] toArray() { return s.toArray(); }
public <T> T[] toArray(T[] a) { return s.toArray(a); }
//下面全是重写Object中的方法
@Override public boolean equals(Object o)
{ return s.equals(o); }
@Override public int hashCode() { return s.hashCode(); }
@Override public String toString() { return s.toString(); }
}
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedSet(Set<E> s) {
super(s);
}
@Override public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
public static void main(String[] args) {
InstrumentedSet<String> s =
new InstrumentedSet<String>(new HashSet<String>());
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
System.out.println(s.getAddCount());
}
}
Set接口的存在使得InstrumentedSet类的设计成Set集合类成为可能,因为Set接口保存了HashSet类的功能特性,除了获得健壮性之外,这种设计也带来了格外灵活性。InstrumentedSet类实现了Set接口,并且拥有单个构造器,它的参数也是Set类型,从本质上讲,这个类把一个Set转变成了另一个Set,同时增加了计数器功能。
因为每一个InstrumentedSet实例都把另一个Set实例包装起来了,所以InstrumentedSet类被称做为包装类,这也正是Decorator装饰模式。InstrumentedSet类对一个集合进行了装饰,为它增加了计数特性。有时,组合和转发的结合也被错误地称为“委托”,从技术的角度而言,这不是委托,除非包装对象(InstrumentedSet)把自身传递给被包装的对象(HashSet)。
组合类几乎没有缺点,但需要注意的一点是,包装类不适合用在回调框架中;在回调框架中,对象把自身的引用传递给其他的对象,所以它传递一个指向自身的引用(this),回调时避开了外面包装对象,这被称为SELF问题。
有些人担心转发方法调用所带来的性能影响,或者包装对象导致的内存占用。在实践中,这两者都不会造成很大的影响。编写转发方法倒有点琐碎,但是只需要给每个接口编写一次构造器,转发类则可以通过包含接口的包替你提供。
只有当子类真正是超类的子类型时,才适合用继承。换句话说,对于两个类A和B,只有当两者之间确实存在“is-a”关系的时候,类B才应该扩展类A。如果你打算让类B扩展类A,就应该问问自己了:每个B确实也是A吗?如果不能确定,通常情况下,B应该包含A的一个私有实例,并且暴露一个较小的、比较简单的API:A本质上不是B的一部分,只是它的实现细节而已。
在Java平台类库中,有许多明显违反这条原则的地方,例如,栈并不是向量,所以Stack不能继承Vecotr;属性列表也不是散列,所以Properties不能继承Hashtable,在这种情况下,复合模式才是恰当的。
如果在适合于使用组合的地方使用了继承,则会不必有地暴露实现细节(如暴露不必要的接口导致外界调用这些接口来非法改变实例的状态)。这样得到的API会把你限制在原始的实现上,永远限定了类的性能,更为严重的是,由于暴露了内部的细节或不必要的接口,客户端就有可能直接访问这些内部细节,从而破坏实现的内部状态。
继承机制会把超API中的所有缺陷传播到子类中,而复合则允许设计新的API来隐藏这些缺陷。
简而言之,继承的功能非常强大,但是也存在诸多的问题,因为它违背了封装的原则。只有当子类和超类之间确实存在子类型关系时,使用继承才是恰当的。即使如此,如果子类和超类外在不同的包中,并且超类并不是为了继承而设计的,那么继承将会脆弱性,为了避免这种脆弱性,可以用复合和转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候。包装为不仅比子类更加健壮,而且功能也更加强大。
17、 要么为继承而设计并提供文档说明,要么就禁止继承
第16条提醒我们,继承自一个不是为了继承而设计、并且没有文档说明的“外来”类是危险的。
如果一个类是专为了继承而设计的,要求有哪些呢?
首先,该类的文档必须精确地描述覆盖每个方法所带来的影响。换句话说,该类必须有文档说明它可覆盖的方法的自用性(self-use)。对于每个公有的或受保护的方法或者构造器,它的文档必须指明该方法或者构造器调用了哪些可覆盖的方法,是以什么顺序调用的,每个调用的结果以是如何影响后续的处理过程的(如在哪些情况下它会调用可覆盖的方法,必须在文档中说明)。
按照惯例,如果方法调用到了可覆盖的方法,在它的文档注释的末尾该包含关于这些调用的描述信息。这段描述信息要以这样的句子开头:“This implementation.(该实现...)”。这样的句子不是在表明该方法可能会随着版本的变迁而改变,而是表明了该方法的内部工作情况。下面是摘自java.util.AbstractCollection的规范:
public boolean remove(Object o):
从此 collection 中移除指定元素的单个实例(如果存在)(可选操作)。更正式地说,如果该 collection 包含一个或多个满足 (o==null ? e==null : o.equals(e)) 的元素 e,则移除 e。如果该 collection 包含指定的元素(或等价元素,如果该 collection 随调用的结果发生变化),则返回 true。
此实现在该 collection 上进行迭代,查找指定的元素。如果找到该元素,那么它会使用迭代器的 remove 方法从该 collection 中移除该元素。注意,如果此 collection 的 iterator 方法所返回的迭代器无法实现 remove 方法,并且此 collection 包含指定的对象,那么此实现会抛出 UnsupportedOperationException。
该文档清楚地说明了,覆盖iterator方法将会影响remove方法的行为。而且,它确切地描述了iterator方法返回的Iterator的行为将会臬影响remove方法的行为。与此相反,在第16条的情形中,程序员在子类化HashSet时,并没有说明覆盖add方法是否会影响addAll方法的行为。
关于程序文档有句格言:好的API文档应该描述一个给定的方法做了什么工作,而不是描述它是如何做到的。而HashSet的add方法违背了这句格言,这正是继承破坏了封装性所带来的不幸后果。所以,为了设计一个类的文档,以便它能够被安全的继承,你必须描述清楚那些有可能未定义的实现细节(引导子类如何实现)。
类可以通过某种形式提供适当的钩子(hook),以便能够进行入到它的内部工作流程,这种形式可以是精心选择的受保护(protected)方法。
为了允许继承,类还必须遵守其他一些约束。构造器决不能调用可被覆盖的方法。因为超类的构造器在子类的构造器之前运行,所以,子类中覆盖版本的方法将会在子类的构造器运行之前就先被调用,如果该覆盖版本的方法依赖于子类构造器所执行的任何初始化工作,该方法将不会如预期般地执行。
如果你决定在一个为了继承而设计的类中实现Cloneable或者Serializable接口,就应该意识到,因为clone和readObject方法在行为上非常类似于构造器,所以类似的限制规则也是适用的:无论是clone还是readObject,都不可以调用可覆盖的方法。对于readObject方法,覆盖版本的方法将在子类的状态被反序列化前先被运行;而对于clone方法,覆盖版本的方法则在子类的clone方法调用来修正 被克隆对象的状态之前先被运行(即父类的clone方法调用了覆盖的方法,而子类的clone却还没调用,这样就可能没有初始化这个覆盖方法所依赖的数据)。
对于普通的具体类应该怎么办?他们即不是final的,也不是为了子类化而设计和编写文档的,所以这种状况很危险。这个问题的最佳解决方案是,对于那些并不能安全进行子类化又没有编写文档的类,要禁止子类化。禁止子类化有两种方法,一种就是定义成final的,另一种就是私有构造器,交提供静态工厂方法,可参考第15条。
这条建议可能会引来争议,因为许多程序员已经习惯了对普通的具体类进行子类化,以便增加新的功能。如果这个普通类实现了某个接口,比如Set、List或者Map,就完全可以禁止该普通类可子类化,因为即使禁止了,我们可以采第16条里的组合与转发即包装模式来提供一种更好的办法;如果具休的类并没有实现标准的接口,那么禁止继承可能会给有些程序员带来不便,如果认为必须允许从这样的类继承,一种合理的办法是确保这个类永远不会自己调用自已的任何可覆盖的方法,并在文档中说明这一点,换句话说,完全消除这个类中可覆盖方法的自用特性,这样做后就可以创建“能够安全地进行子类化”的类。
当然,你可以使用一种机械的做法来消除可覆盖方法的自用特性,而不改变它的功能行为。就是直接将被调用的可覆盖方法的实现代码Copy到另外可覆盖方法中,让它们不相互依赖调用,那么此时子类就可以继承并覆盖这些方法了。
18、 接口优于抽象类
如果这些方法尚不存在,你所需要做的就是只是增加必要的方法,然后在类的声明中增加一个implements子句,实现抽象方法即可。但一般来说,你无法直接修改当前类来扩展新的抽象类,此时你只能将抽象类从继承树的最底向上移到最顶层,
实现新的接口,现有类可以很容易被更新:从代码重构的角度上讲,将一个单独的Java具体类重构成一个实现某个Java接口是很容易的,只需要声明一个Java接口,并将重要的方法添加到接口声明中,然后在具体定义语句后面加上一个合适的implements关键字即可, 例如,当Comparable接口被引入到Java平台中时,会更新许多现有的类,以实现Comparable接口;而为一个具体类添加一个抽象类作为抽象类型却不是那么容易,因为这个具体类可能已经有了一个超类。这样一来,这样新定义的抽象类只好继续向上移动,变成这个超类的超类,这样,最后这个新定义的抽象类必定处于整个类型等级结构的最上端,从而使等级结构中的所有成员都会受到影响,无论这个抽象类对于这些后代类是否合适。
接口是定义混合类型的理想选择:混合类型就是类除了实现或继承了它的“基本类型(也可叫主要类型)”之外,还可以实现这个混合类型,以表明它提供了某些可供选择的行为。例如,Comparable就是一个混合接口,它允表明实现了Comparable混合接口的实例可以与其他的可相互比较的对象进行排序。抽象类是不能被用于定义混合类型的,同样也是因为它们不能被添加到现有的类中:类不可能有一个以上的父类,类层次结构中也没有适当的地方来插入这种混合类型。
接口允许我们构造非层次结构的类型框架:类型层次对于组织某些事物是非常合适的,但是其他有些事物并不能被整齐地组织成一个严格的层次结构。例如,假设我们有一个接口代表一个Singer(歌唱家),另一个接口代表一个SongWriter(作曲家):
public interface Singer{
AudioClip sing(Song s);
}
public interface SongWriter{
Song compose(boolean hit);
}
在现实生活中,有些歌唱家本身也是作家。因为我们这里使用了接口而不是抽象类来定义这些类型,所以对于单个类而言,它同时实现Singer和SongWriter是完全允许的。实际上,我们可以定义第三个接口,它同时扩展了Singer和SongWriter,并添加了一些适合于这种组合的新方法:
public interface SingerSongwriter extends Singer,SongWriter{
AudioClip strum();
void actSensitive();
}
你也许并不总需要这种灵活性,但是一旦你这样做了,接口可就成了救世主,能帮助你解决大问题,因为这种非层关系的类我们可以自由的组合。这里另外一种做法是编写一个臃肿的类层次,对于每一种要被支持的属性组合,都包含一个单独的类。如果在整个类型系统中有n个属性,那么就必须支持2^n种可能的组合,这种现象被称为“组合爆炸”,类层次的臃肿会导致类也臃肿,这些类包含许多方法,并且这些方法只是在参数类型上有所不同而已,因为类层次中没有任何类型体现了公共的行为特征。所以这里如果使用抽象类来定义的话显示是不合适的,
虽然接口不允许包含方法的实现,但是,使用接口来定义类型并不妨碍你为程序提供实现上的帮助。通过对你导出的每个重要接口都提供一个抽象类,将公用的方法实现,把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但是抽象类接管了所有与接口实现相关的工作。
按照惯例,我们一般将提供的抽象类命名为AbstractInterface这种形式,这里的Interface是指所实现的接口的名字。例如,集合框架为每个重要的集合接口都提供了一个骨架实现,如AbstractCollection、AbstractSet、AbstractList和AbstractMap。再来看看这些集合类的定义:
public class ArrayList extends AbstractList implements List{}
public class HashSet extends AbstractSet implements Set{}
public class HashMap extends AbstractMap implements Map{}
如果设计得当,抽象类可以使用程序员很容易就提供他们自己的接口实现。例如,下面是一个静态工厂方法,它包含一个完整的、功能全面的List实现:
public class IntArrays {
static List<Integer> intArrayAsList(final int[] a) {
if (a == null)
throw new NullPointerException();
return new AbstractList<Integer>() {
public Integer get(int i) {
return a[i]; // Autoboxing (Item 5)
}
@Override public Integer set(int i, Integer val) {
int oldVal = a[i];
a[i] = val; // Auto-unboxing
return oldVal; // Autoboxing
}
public int size() {
return a.length;
}
};
}
public static void main(String[] args) {
int[] a = new int[10];
for (int i = 0; i < a.length; i++)
a[i] = i;
List<Integer> list = intArrayAsList(a);
Collections.shuffle(list);
System.out.println(list);
}
}
编写抽象类相对比较简单,只是有点单调乏味。首先,必须认真研究接口,并确定哪些方法是最为基本的,其他的方法则可以根据它们来实现。这些基本的方法将成为抽象类中的抽象方法。然后,必须为接口中所有其他的方法提供具体的实现。例如,下面是Map.Entry接口的抽象类:
public abstract class AbstractMapEntry<K,V> implements Map.Entry<K,V> {
// 基本操作,定义为抽象的
public abstract K getKey();
public abstract V getValue();
// 可修改的Map中的实体Entry需要实现
public V setValue(V value) {
throw new UnsupportedOperationException();
}
// 实现共(通)用接口Map.Entry.equals
@Override public boolean equals(Object o) {
if (o == this)
return true;
if (! (o instanceof Map.Entry))
return false;
Map.Entry<?,?> arg = (Map.Entry) o;
return equals(getKey(), arg.getKey()) &&
equals(getValue(), arg.getValue());
}
private static boolean equals(Object o1, Object o2) {
return o1 == null ? o2 == null : o1.equals(o2);
}
// 实现共(通)用接口Map.Entry.hashCode
@Override public int hashCode() {
return hashCode(getKey()) ^ hashCode(getValue());
}
private static int hashCode(Object obj) {
return obj == null ? 0 : obj.hashCode();
}
}
抽象类的唯一优点就是抽象类的演变比接口的演变要容易得多:如果在后续的发行版本中,你希望在抽象类中增加新的方法,你始终可以增加具体方法,如果向一个抽象类加入一个新的具体方法,那么所有的子类一下子就都得到了这个新的具体方法,而Java接口做不到这一点,如果向一个Java接口加入一个新方法的话,所有实现这个接口的类就不能通过编译了,因为它们都没有实现这个新声明的方法。这显然是Java接口的一个缺点。
由于Java抽象类具有提供缺省实现的优点,而Java接口具有其他所有的优点,所以联合使用就是一个很好的选择。如果一个具体类直接实现这个Java接口的话,它就必须自行实现所有的接口;相反,如果它继承自抽象类的话,它可以省去一些不必要的方法,因为它可以从抽象类中自动得到这些方法的缺省实现。如果需要向Java接口加入一个新的方法的话,那么只要同时向这个抽象类加入这个方法的一个具体实现就可以了,因为所有继承自这个抽象类的子类都会从这个抽象类得到这个具体方法。这其实就是一种缺省适配器模式。一般来说,要想在公开的接口增加方法,而不破坏实现这个接口的所有现有类,这是不可能的,除非像上面说的那样,一开始就让某个类实现接口的时候,也继承抽象类,但这是不完全可能的,所以不从抽象类继承的接口实现类仍然会无法编译。
因此,设计公有接口要非常谨慎,接口一旦被公开发行,并且已被广泛实现,再想改变这个接口几乎是不可能的。你必须在初次设计的时候就保证接口是正确的。如果接口包含即使微小的瑕疵,它将会一直影响接口用户。如果接口具有严重缺陷,它可以导致API彻底的失败。
简而言之,接口通常是定义允许多个实现的类型的最佳途径。这条规则有个例外,即当演变的容易性比灵活性和功能更为重要的时候,在这种情况下,应该使用抽象类来定义类型,但前提是必须理解并且可以接受抽象类的局限性。如果你导出一个重要接口,就应该坚决考虑同时提供一个抽象类。最后,应该尽可能谨慎地设计所有公有接口,一旦发行,将不可更改。
19、 接口只用于定义类型
当类实现接口时,接口就充当可以引用这个类的实例的类型。因此,类实现了接口,就表明客户端可以对这个类的实例实施某些动作。为了任何其他目的而定义接口是不恰当的。
有一种接口称为常量接口,它违反了上面的条件,这种接口不包含任何方法,它只包含静态的final域,每个域都导出一个常量。使用这些常量的类实现这个接口,以避免用类名来修饰常量名:
public interface PhysicalConstants {
static final double AVOGADROS_NUMBER = 6.02214199e23;
}
class Sub implements PhysicalConstants {
public static void main(String[] args) {
System.out.println(AVOGADROS_NUMBER);
}
}
常量接口模式是对接口的不良使用。类在内部使用某些常量,这纯粹是实现细节。实现常量接口,会导致把这样的实现细节泄露到该类的导出API中。类实现常量接口没有什么价值。如果在将来的发行版本中,这个类被修改了,它不再需要使用这些常量了,它依然必须实现这个接口,以确保二制兼容性。如果非final类实现了常量接口,它的所有子类的命名空间也会被接口中的常量所“污染”。Java平台类库中有几个常量接口,例如java.io.ObjectStreamConstants,被认为是反面例子,不值得效仿。
如果要导出常量,可以有几种合理的方案。如果这些常量与某个现有的类或者接口紧密相关,就应该把这些常量添加到这个类或者接口。例如,在Java平台类库中所有的数值包装类,如果Integer和Double,都导出了MIN_VALUE和MAX_VALUE常量。如果这些常量最好被看作枚举类型的成员,就应该使用枚举类型(见第30条)来导出这些常量。否则,应该使用不可实例化的工具类(见第4条)来导出这些常量,下面是前面的PhysicalConstants例子的工具类翻版:
public class PhysicalConstants {
private PhysicalConstants() { } // 私有构造器
public static final double AVOGADROS_NUMBER = 6.02214199e23;
}
如果大量利用工具类导出的常量,可以通过利用静态导入机制,避免用类名来修饰常量,不过,静态导入机制是在1.5中才引用的。
简而言之,接口应该只被用来定义类型,它们不应该被用来导出常量。
20、 类层次优于标签类
有时候,可能会遇到带有两种甚至更多风格(功能)的实例的类,并包含表示实例风格的标签域,例如:
class Figure {
enum Shape { RECTANGLE, CIRCLE };
// 标签域 - 是圆形还是长方形
final Shape shape;
// 这两个域仅用于长方形
double length;
double width;
// 这个域仅用于圆形
double radius;
// 圆形构造器
Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}
// 长方形构造器
Figure(double length, double width) {
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
}
// 求面积
double area() {
//不同的形状有不同的算法
switch(shape) {
case RECTANGLE:
return length * width;
case CIRCLE:
return Math.PI * (radius * radius);
default:
throw new AssertionError();
}
}
}
这种标签类有着许多缺点。它们中充斥着样板代码(同一时刻只用到一种功能),包括枚举声明、标签域以及条件语句。破坏了可读性,内存占用也增加,因为实例承担着属于其他风格的不相关的域。总之,标签类过于冗长、容易出错、并且效率低下。
使用子类化能更好地设计这个类。为了将标签转变成类层次,首先要为每个依赖于标签的方法都定义成一个抽象方法并放在抽象类中。在Figure类中,只有一个这样的方法:area。这个抽象类是类层次的根。如果还有其他的方法其行为不依赖于标签域,就应该把这样的方法放在这个抽象类中。同样地,如果所有的方法都用到了某些数据域,应该把它们放在这个抽象类中。不过,在Figure类中,不存在这种类型独立的方法或者数据域,以下是重构:
abstract class Figure {
abstract double area();
}
class Circle extends Figure {
private final double radius;
Circle(double radius) { this.radius = radius; }
double area() { return Math.PI * (radius * radius); }
}
class Rectangle extends Figure {
private final double length;
private final double width;
Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
double area() { return length * width; }
}
所有的域都是final的,编译器确保每个类的构造器都初始化它的数据域,对于根类中声明的每个抽象方法,都确保有一个实现,这样杜绝了由于遗漏swithc case而导致运行时失败的可能性。这个类层次纠正了前面提到过的标签类所有缺点。
类层次的另一好处在于,它们可以用来反映类之间本质上的层次关系,有助于后期的扩充。假设现有加一个正方形有,标签类就需要修改源码,而利用类层次结构只需新加一个正文型类,并继承自长方形,这样可以反映出它是一种特殊的长方性:
class Square extends Rectangle {
Square(double side) {
super(side, side);
}
}
简而言之,标签类很少有适用的时候,当你想使用的时,这个标签是否可以被取消,这个类是否可以用类层次来代替。当你遇到一个包含标标签域的现有类时,就要考虑将它重构到一个层次结构中去。
21、 用函数对象表示策略
Java没有提供函数指针,但是可以用对象引用实现同样的功能。
考虑下面的类:
public class StringLengthComparator {
public int comare(String s1, String s2) {
return s1.length() - s2.length();
}
}
它是一个字符长度比较器,只有这样的一个比较功能的方法,如果一个类仅仅导出某个特定功能的方法,它的实例实际上就等同于一个指向该方法的指针,这样的实例被称为函数对象。这是一种策略的应用,换句话说,StringLengthComparator实例是用于字符串比较操作的具体策略对象。
作为典型的具体策略类,StringLengthComparator类是无状态的:它没有域,所以,这个类的所有实例在功能上都是相互等价的。因此,它作一个Singleton是非常合适的,可以节省不必要的对象创建开销(见第3与第5条):
public class StringLengthComparator {
private StringLengthComparator(){}
public int comare(String s1, String s2) {
return s1.length() - s2.length();
}
}
为了把StringLengthComparator实例传递给方法,需要适当的参数类型,直接使用StringLengthComparator并不好,因为客户端将无法传递任何其他的比较策略,即不能随时动态的改变比较性为。此时,我们可以定义一个比较策略接口,这个接口在java.util包就已提供,我们不需要再提供,我们直接实现它:
import java.util.Comparator;
public class StringLengthComparator implements Comparator<String> {
private StringLengthComparator() {}
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
具体的策略类往往使用匿名类(见第22条)来声明,下面的语句根据长度对一个字符串数组进行排序:
Arrays.sort(strArr,new Comparator<String>(){
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}});
但是注意,以这种方式使用匿名类时,将会在每次执行调用的时候创建一个新的实例。如果它被重复执行,考虑将函数对象存储到一个私有的静态final域时重复使用它。
因为策略接口被用做所有具体策略实例的类型,所以我们并不需要为了导出具体策略,而把具体策略类做成公有的。我们可以把具体策略类定义成静态的内部类,让外部类直接导出公有的静态策略对象域(或定义成私有的后通过静态的工厂方法返回),其类型为策略接口。下面的例子使用静态成员类,而不是匿名类,这样允许我们的具体的策略类实现第二个接口Serializable:
public class Outer {
private static class StrLenCmp implements Comparator<String>, Serializable {
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
public static final Comparator<String> STRING_LEN_CMP = new StrLenCmp();
//...
}
这新我们很容易改变比较的策略算法,比如要导出一个不区分大小定的字符串比较器就很容易了,只要提供的接口不变。
简而言之,函数的主要用途就是实现策略模式。为了在Java中实现这种模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的策略类。当一个具体策略只被使用一次时,通常使用匿名类来声明和实例化这个具体匿名策略类。当一个具体策略是设计用来重复使用的时候,它的类通常要被实现为私有的静态成员类,并通过公有的静态final域被导出,其类型为该策略接口。
22、 四种嵌套类中优先考虑静态成员类
嵌套类是指被定义在一另一个类的内部的类。它的目的只为外围类提供服务,如果将来它可能会单独用于其他的某个环境中,它应该是顶层类。嵌套类有四种:静态成员类、非静态成员类、匿名类、局部类,除了第一种,其他三种被称为内部类,静态与非静态成员类又称为成员类。
静态成员类的一种常见用法是作为公有的辅助类,仅当与它的外部类一起使用时才有意义。例如,这样的一个枚举类,它描述了计算器支持的各种操作(见30条)。Operation枚举应该是Calculator类的公有静态成员类,然后,Calculator类的客户端就可以采用如Calculator.Operation.PLUS和Calculator.Operation.MINUS这样的名称来引用这些操作。
如果嵌套类的实例可以在它外围类的实例之外独立存在,这个嵌套类就必须是静态成员类:在没有外围实例的情况下,要想创建非静态成员类的实例是不可能的。
非静态成员类一种常见用法是定义一个Adapter(适配器,将Map集合转换成Set或Collection接口),它允许外部类的实例被看作是另一个不相关的类的实例。例如,Map接口的实现往往使用非静态成员类来实现它们的集合视图,这些集合视图由Map的entrySet、keySet、values方法返回的,同样,如Set和List这种集合接口的实现往往也使用非静态成员类来实现它的迭代器,下面看看HashMap的组成部分:
public class HashMap extends AbstractMap implements Map, Cloneable, Serializable{
//静态实体组件类
static class Entry implements Map.Entry {
final Object key;
Object value;
final int hash;
Entry next;
//...
}
//HashMap的抽象迭代器,迭代出的是一个个Entry。专用来被key、value迭代器继承
private abstract class HashIterator implements Iterator {
//...
Entry nextEntry() {
//...
}
}
// 对Map中的value进行迭代
private class ValueIterator extends HashIterator {
public Object next() {
return nextEntry().value;
}
}
// 对Map中的key进行迭代
private class KeyIterator extends HashIterator {
public Object next() {
return nextEntry().getKey();
}
}
//Entry迭代器
private class EntryIterator extends HashIterator {
public Object next() {
return EntryIterator ();
}
}
// 下面的集合视图都是建立在上面迭代器之上的
// Key视图
private class KeySet extends AbstractSet {
public Iterator iterator() {
return KeyIterator ();
}
//...
}
//value视图
private class Values extends AbstractCollection {
public Iterator iterator() {
return ValueIterator ();
}
// ...
}
如果声明成员类不要求访问外围实例,就应加上static修饰符。如果省略了,则每个实例都将包含一个额外指向外围对象的引用,保存这份引用要消耗时间和空间,并且会导致外围实例在符合垃圾回收(见第6条)时却仍然得到保留。
私有静态成员类的一种常见用法是用来代表外围类所代表的对象的组件。Map实现的内部都有一个Entry类,表示Map中的每个健-值对。虽然每个entry都与一个Map关联,但是entry上的方法(getKey、getValue和setValue)并不需要访问该Map,因此,使用非静态成员来表示entry是很浪费的:私有的静态成员类是最好的选择。如果漏掉了Entry声明成static,该Map仍可以工作,但是每个entry中将会包含一个指向Map的引用,这样就浪费空间和时间了。
非静态的内部类中不能定义static成员,但可以是定义final static成员。因为一个成员类实例必然与一个外部类实例关联,这个static定义完全可以移到其外围类中去。
当且仅当匿名类出现在非静态环境中时,它才有外围实例。但是如果它出现在静态的环境中,如静态的方法中时,就没不会指向外围类对象。而且匿名类不能包含静态的非final成员。
匿名类的适用性受到很多的限制,除了在它们声明的同时实例化外,不可以在其他地方实例化。你不能执行instanceof测试,或者做任何需要命名类的其他事情。你无法声明在一个匿名类上再实现一个或多个接口,或者是继承一个类。匿名类的客户端无法调用到它里面定义的父类中没有成员,而只能是访问到父类或接口中的成员,因为客户端引用父类类型。另外,由于匿名类出现在表达式中,它们必须保持简短——大约10行或更少——否则会影响程序的可读性。
匿名类的一种常见用法是动态地创建函数对象(见第21条),与策略接口或回调接口一起使用;匿名类的另一种常见用法是创建过程对象,比如Runnable、Thread或者TimerTask实例;第三种常见的用法是在静态工厂方法的内部(见第18条中的intArrayAsList方法)。
局部类是四种嵌套类中用得最少的类。在任何“可以声明局部变量”的地方,都可以声明局部类,并且也只能在当前作用域类使用。局部类与其他三种嵌套类中的每一种都有一些共同点:与成员类(静态与非静态成员类)一样,局部类有名字,可以被重复地使用;与匿名类一样,只有当局部类是在非静态环境中定义的时候,才有外围实例,与匿名类一样局部类中都不能定义静态的非final成员。与匿名类一样,它们也必须非常简短,以便不会影响到可读性。下面是静态与非静态环境中的局部类,它们是否拥有指向外围类的引用:
public class Outer {
static void f() {//静态方法中的内部类没有外围对象引用
class Inner {
static final int i = 1;
}
}
void g() {
class Inner {}
}
}
使用javap反编译后如下:
class Outer$1Inner extends java.lang.Object{
static final int i;
Outer$1Inner();
}
class Outer$2Inner extends java.lang.Object{
final Outer this$0; //非静态方法中的内部类有外围对象引用
Outer$2Inner(Outer);
}
静态方法中的局部类为静态的,没有指向外围类实例,非静态方法中的局部类有指向外围实例的引用(从上面反编译可以看出)。
另外,不管是在静态的还是非静态的环境中定义的局部或匿名类,都不是static的(所以为什么局部或匿名类不能定义静态的非final成员了),或者更严格的讲,没有静态的局部或匿名类,因为局部环境中是不可以使用static修饰的。
简而言之,四种不同的嵌套类,都有自己的用途。如果一个嵌套类需要在单个方法之外仍然是可见的,或者它太长了,不适合于放在方法内部,就该使用成员类。如果成员类的每个实例都需要一个指向其外围实例的引用,就要把成员类做成非静态的,否则做成静态的。假设这个嵌套类属于一个方法的内部,如果你只需要在一个地方创建实例,并且已经有了一个预置的类型接口可以说明这个类的特征,就要把它做成匿名类,否则,就做成局部类吧。