ITEM 24: FAVOR STATIC MEMBER CLASSES OVER NONSTATIC
嵌套类是定义在另一个类中的类。嵌套类的存在只是为了服务于它的外部类。如果嵌套类在其他上下文中有用,那么它应该是顶级类。有四种嵌套类:静态成员类、非静态成员类、匿名类和本地类。除了第一种类之外,所有的类都称为内部类。本条告诉您何时使用哪种嵌套类以及为什么使用。
静态成员类是最简单的嵌套类。最好将它看作一个普通类,它恰好在另一个类中声明,并且可以访问所有封闭类的成员,甚至那些声明为私有的成员。静态成员类是其封闭类的静态成员,并且遵循与其他静态成员相同的可访问性规则。如果它被声明为私有,则只能在封闭的类中访问它,以此类推。
静态成员类的一个常见用途是作为公共助手类,仅与它的外部类一起使用。例如,考虑一个枚举,它描述计算器支持的操作(item 34)。操作enum 应该是 Calculator 类的一个公共静态成员类。然后 Calculator 的客户端可以使用 Calculator.Operation.PLUS 和 Calculator.Operation.MINUS 。
从语法上讲,静态成员类和非静态成员类之间的惟一区别是,静态成员类的声明中有修饰符static。尽管语法相似,但这两种嵌套类非常不同。非静态成员类的每个实例都隐式地关联到其包含类的一个封闭实例。在非静态成员类的实例方法中,可以调用封闭实例上的方法,或者使用限定的 this 指针获取对封闭实例的引用。如果嵌套类的实例可以独立于其封闭类的实例存在,那么嵌套类必须是静态成员类:如果没有封闭实例,就不可能创建非静态成员类的实例。
非静态成员类实例与其封闭实例之间的关联是在创建成员类实例时建立的,此后不能修改。通常,关联是通过从封闭类的实例方法中调用非静态成员类构造函数自动建立的。使用 enclosingInstance.new MemberClass(args) 手动建立关联是可能的,尽管这种情况很少见。正如您所期望的,关联会占用非静态成员类实例中的空间,并为其构造增加时间。
非静态成员类的一个常见用法是定义适配器[Gamma95],该适配器允许将外部类的实例视为一些不相关类的实例。例如,Map接口的实现通常使用非静态成员类来实现它们的集合视图,集合视图由Map的keySet、entrySet和values方法返回。类似地,集合接口的实现,如Set和List,通常使用非静态成员类来实现它们的迭代器:
// Typical use of a nonstatic member class
public class MySet<E> extends AbstractSet<E> {
... // Bulk of the class omitted
@Override public Iterator<E> iterator() { return new MyIterator();}
private class MyIterator implements Iterator<E> { ...}
}
如果声明不需要访问封闭实例的成员类,则始终将静态修饰符放在其声明中,使其成为静态成员类而不是非静态成员类。如果省略此修饰符,则每个实例都有一个与其所包含实例无关的隐藏引用。如前所述,存储这个引用需要时间和空间。更严重的是,它可能会导致封闭实例被保留,而它本来可以被垃圾收集(item 7)。通常很难检测到,因为引用是不可见的。
私有静态成员类的一个常见用途是表示由其封闭类表示的对象的组件。例如,考虑一个Map实例,它将键与值关联起来。许多映射实现为映射中的每个键值对都有一个内部条目对象。虽然每个条目都与映射相关联,但是条目上的方法(getKey、getValue和setValue)不需要访问映射。因此,使用非静态成员类来表示条目是很浪费的:私有静态成员类是最好的。如果您不小心忽略了条目声明中的静态修饰符,映射仍然可以工作,但是每个条目都包含对映射的多余引用,这会浪费空间和时间。
如果所讨论的类是导出类的公共成员或受保护成员,那么在静态成员类和非静态成员类之间进行正确的选择是非常重要的。在这种情况下,成员类是导出的API元素,不能在后续版本中从非静态类更改为静态成员类而不违反向后兼容性。
正如您所期望的,匿名类没有名称。它不是其封闭类的成员。它不是与其他成员一起声明,而是在使用时同时声明和实例化。在代码中表达式合法的任何地方都允许使用匿名类。当且仅当匿名类出现在非静态上下文中时,它们才包含实例。但是,即使它们发生在静态上下文中,它们也不能有除常量变量之外的任何静态成员,常量变量是初始化为常量表达式的最终基元或字符串字段[JLS, 4.12.4]。
匿名类的适用性有很多限制。您不能实例化它们,除非在声明它们的时候。您不能执行 instanceof 测试或任何其他需要您命名类的操作。您不能声明一个匿名类来实现多个接口,或同时扩展一个类和实现一个接口。匿名类的客户端除了从其超类型继承的成员外,不能调用任何成员。因为匿名类发生在表达式的中间,所以它们必须保持简短—大约 10 行或更少 —— 否则可读性将受到影响。
在将 lambdas 添加到 Java 之前,匿名类是动态创建小函数对象和进程对象的首选方法,但是 lambdas 现在是首选方法(item 42)。匿名类的另一个常见用法是在静态工厂方法的实现中(请参见 item 20 中的intArrayAsList)。
在四种嵌套类中,本地类的使用频率最低。局部类实际上可以在任何可以声明局部变量的地方声明,并且遵循相同的作用域规则。本地类与其他类型的嵌套类具有相同的属性。与成员类一样,它们也有名称,可以重复使用。与匿名类一样,只有在非静态上下文中定义实例时,它们才包含实例,并且不能包含静态成员。和匿名类一样,它们应该保持简短,以免影响可读性。
概括一下,有四种不同的嵌套类,每种类都有自己的位置。如果嵌套类需要在单个方法外部可见,或者太长而不能很好地放入方法内部,则使用成员类。如果成员类的每个实例都需要对其封闭实例的引用,则使其非静态;否则,让它保持静态。假设类属于一个方法,如果您只需要从一个位置创建实例,并且有一个预先存在的类型来描述该类,则将该类设置为匿名类;否则,让它成为一个本地类。
网友评论