嵌套类(nested class)是一个定义在另外一个类内部的类。嵌套类应该仅仅是为了服务外部类而存在。如果内嵌类在其他某些情形下有用,那么他应该是一个顶层类。有四种嵌套类:静态成员类(static member class)、非静态成员类(nonstatic member class)、匿名类(anonymous class)和本地类(local class)。只有第一种是被认为是内部类。这个条目告诉你声明时候使用哪种嵌套类以及为什么。
静态成员类是嵌套类的最简单的种类。它最好被认为是一个普通类,它只是恰好声明在另外一个类的内部,而且可以访问外部类的所有成员,即使这些成员是声明为私有的。静态成员类是它的外部类的静态成员,而且就像其他静态成员一样遵从同样的访问规则。如果它是声明为私有的,那么他仅仅可以在外部类内部访问,诸如此类。
静态内部类的一个通常使用是作为一个公开协助类,仅仅是和它的外部类联合使用。例如,考虑一个枚举,它描述由计算器支持的操作 (条目34)。Operation枚举应该是Calculator类的公开静态成员。于是Calculator的客户端应该引用一些操作,这些操作使用像Calculator.Operation.PLUS和Calculator.Operation.MINUS这样的名字。
在语句构成上,静态和非静态成员类的唯一区别在于,静态成员类在它们的声明中有修饰符static。尽管语法上的相似性,但是这两种嵌套类是非常不同的。非静态成员类的每个实例与包含类的外部实例相关联。在非静态成员类的实例方法里面,你可以调用外部实例(enclosing instance)的方法,或者使用限定的(qualified)this结构体获得外部实例的引用 [JLS, 15.8.4]。如果嵌套类的实例的存在脱离它的外部类的实例,那么嵌套类必须是一个静态成员类:没有外部实例,创建一个非静态成员类是不可能的。
当成员类实例创建时,非静态成员类实例和它的外部实例的联系建立,而且在这之后不能改变。通常,这个联系是从外部类的实例方法内部,通过调用一个非静态成员类构造子自动建立的。手动使用enclosingInstance.new MemberClass(args)建立这个联系,也是可能的,虽然非常少见。就像你所预料的,这个联系在非静态成员类实例中占用了空间,而且它的构造过程增添了时间。
非静态成员类的一个通常使用是定义一个Adapter[Gamma95],它允许外部类的实例被看成是某个不相关类的实例。例如,Map接口的实现通常使用非静态成员类实现他们的集合视图(collection view),它们是由Map’s keySet、entrySet和values返回。相似地,集合接口,比如Set和List,它们的实现通常使用非静态成员类来实现它们的迭代器:
// 非静态成员类的典型使用
public class MySet<E> extends AbstractSet<E> {
... // 这个类的大部分省略
@Override public Iterator<E> iterator() {
return new MyIterator();
}
private class MyIterator implements Iterator<E> {
...
}
}
如果你定义一个成员类,它不需要访问外部实例,那么永远把static修饰符放在它的声明中,让它成为一个静态而不是非静态成员类。如果你省略这个修饰符,那么每个实例将有一个它的外部实例的额外隐含引用。就像前面提到的,存储这个引用耗费时间和空间。更为严重的是,它可能导致外部实例留存,原本它是适合垃圾收集(条目7)。最终的内存泄漏可能是灾难性的。它通常很难监测到,因为这个引用是不可见的。
私有静态成员类的一个通常使用是代表对象的组件,这个对象由它们外部类表示。例如,考虑Map实例,它把键和值联系起来。许多Map实例,对于映射中每个键值对,有一个内部Entry实例。虽然每个entry和一个映射相联系,但是entry的方法(getKey、getValue和setValue)不需要访问映射。所以,使用非静态成员类来表示entry是很浪费的:私有静态成员类是最好的。如果你在entry声明中不慎忽略了static修饰符,这个映射仍然是工作的,但是每个entry将包含一个多余的对映射的引用,这就浪费了空间和时间。
如果正在讨论的类是一个导出类的公开或者受保护成员,在一个静态和非静态成员类之间正确地选择是非常重要的。在这种情况下,成员类是一个导出API元素,而且在后续发布中不能把它从非静态改变为静态成员类,而不会违反向后兼容性。
就像你所预料的,匿名类没有名字。它不是它的外部类的一个成员。不是和其他成员一起声明,它是在使用时同时声明和实例化的。代码中一个表达式合法的任何地方,匿名类也是允许的。当且仅当它们在非静态环境中发生,匿名类有外部类。但是即使它们在静态环境中发生,它们也不能够有任何静态成员,除了常数变量(constant variable),它是初始化为常数表达式的final原始或者字符串域,[JLS, 4.12.4]。
匿名类的应用有许多限制。除非在它们被声明时,你不能够实例化它们。你不能进行instanceof测试,或者做需要你命名这个类的任何其他事情。你不能声明一个匿名类来实现多个接口,或者同时扩展一个类和实现一个接口。使用匿名类的客户端不能够调用任何成员,除了从它的超类继承而来的那些成员。因为匿名类发生在表达式之中,它们应该保持简短(大约十行或者更少),否则可读性将会受损。
在Java添加lambda(第6章)之前,匿名类是随手创建小函数对象(function object)和处理对象(process object)的优选方式,而且,但是lambda现在是更优的(条目42)。匿名类的另外一个通常使用是静态工厂方法的实现(参考条目20中intArrayAsList)。
本地类是四种内嵌类中最少使用。一个本地类实际上可以在一个本地变量可以声明的任何地方声明,而且遵从相同的作用域规则。本地类和其他类型的嵌套类有许多相同的特质。像成员类,它们有名字,而且可以重复使用。像匿名类,只有当它们在非静态情形中定义时,它们有外部实例,而且它们不能保护静态成员。而且像匿名类,为了不伤害可读性,它们应该保持简短。
简要概括,有四种不同嵌套类,而且每种有它的位置。如果一个嵌套类需要在单个方法的外部可见,或者太长了而不适合在一个方法内部,那么使用成员类。如果成员类的每个实例需要它外部实例的引用,那么把它变成非静态;否则,把它变成静态。假设类属于一个方法的内部,如果你需要仅仅从一个位置创建实例,而且有描述这个类的特性的已存类,那么把它变成匿名类;否则,把它变成本地类。