Effective Java 第三版——24. 优先考虑静态成员类

Tips
《Effective Java, Third Edition》一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化。
在这里第一时间翻译成中文版。供大家学习分享之用。

Effective Java 第三版——24. 优先考虑静态成员类

嵌套类(nested class)是在另一个类中定义的类。 嵌套类应该只存在于其宿主类(enclosing class)中。 如果一个嵌套类在其他一些情况下是有用的,那么它应该是一个顶级类。 有四种嵌套类:静态成员类,非静态成员类,匿名类和局部类。 除了第一种以外,剩下的三种都被称为内部类(inner class)。 这个条目告诉你什么时候使用哪种类型的嵌套类以及为什么使用。

静态成员类是最简单的嵌套类。 最好把它看作是一个普通的类,恰好在另一个类中声明,并且可以访问所有宿主类的成员,甚至是那些被声明为私有类的成员。 静态成员类是其宿主类的静态成员,并遵循与其他静态成员相同的可访问性规则。 如果它被声明为private,则只能在宿主类中访问,等等。

静态成员类的一个常见用途是作为公共帮助类,仅在与其外部类一起使用时才有用。 例如,考虑一个描述计算器支持的操作的枚举类型(条目 34)。 Operation枚举应该是Calculator类的公共静态成员类。 Calculator客户端可以使用Calculator.Operation.PLUSCalculator.Operation.MINUS等名称来引用操作。

在语法上,静态成员类和非静态成员类之间的唯一区别是静态成员类在其声明中具有static修饰符。 尽管句法相似,但这两种嵌套类是非常不同的。 非静态成员类的每个实例都隐含地与其包含的类的宿主实例相关联。 在非静态成员类的实例方法中,可以调用宿主实例上的方法,或者使用限定的构造[JLS,15.8.4]获得对宿主实例的引用。 如果嵌套类的实例可以与其宿主类的实例隔离存在,那么嵌套类必须是静态成员类:不可能在没有宿主实例的情况下创建非静态成员类的实例。

非静态成员类实例和其宿主实例之间的关联是在创建成员类实例时建立的,并且之后不能被修改。 通常情况下,通过在宿主类的实例方法中调用非静态成员类构造方法来自动建立关联。 尽管很少有可能使用表达式enclosingInstance.new MemberClass(args)手动建立关联。 正如你所预料的那样,该关联在非静态成员类实例中占用了空间,并为其构建添加了时间开销。

非静态成员类的一个常见用法是定义一个Adapter [Gamma95],它允许将外部类的实例视为某个不相关类的实例。 例如,Map接口的实现通常使用非静态成员类来实现它们的集合视图,这些视图由Map的keySetentrySetvalues方法返回。 同样,集合接口(如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> {
        ...
    }
}

如果你声明了一个不需要访问宿主实例的成员类,总是把static修饰符放在它的声明中,使它成为一个静态成员类,而不是非静态的成员类。 如果你忽略了这个修饰符,每个实例都会有一个隐藏的外部引用给它的宿主实例。 如前所述,存储这个引用需要占用时间和空间。 更严重的是,并且会导致即使宿主类在满足垃圾回收的条件时却仍然驻留在内存中(条目 7)。 由此产生的内存泄漏可能是灾难性的。 由于引用是不可见的,所以通常难以检测到。

私有静态成员类的常见用法是表示由它们的宿主类表示的对象的组件。 例如,考虑将键与值相关联的Map实例。 许多Map实现对于映射中的每个键值对都有一个内部的Entry对象。 当每个entry都与Map关联时,entry上的方法(getKeygetValuesetValue)不需要访问Map。 因此,使用非静态成员类来表示entry将是浪费的:私有静态成员类是最好的。 如果意外地忽略了entry声明中的static修饰符,Map仍然可以工作,但是每个entry都会包含对Map的引用,浪费空间和时间。

如果所讨论的类是导出类的公共或受保护成员,则在静态和非静态成员类之间正确选择是非常重要的。 在这种情况下,成员类是导出的API元素,如果不违反向后兼容性,就不能在后续版本中从非静态变为静态成员类。

正如你所期望的,一个匿名类没有名字。 它不是其宿主类的成员。 它不是与其他成员一起声明,而是在使用时同时声明和实例化。 在表达式合法的代码中,匿名类是允许的。 当且仅当它们出现在非静态上下文中时,匿名类才会封装实例。 但是,即使它们出现在静态上下文中,它们也不能有除常量型变量之外的任何静态成员,这些常量型变量包括final的基本类型,或者初始化常量表达式的字符串属性[JLS,4.12.4]。

匿名类的适用性有很多限制。 除了在声明的时候之外,不能实例化它们。 你不能执行instanceof方法测试或者做任何其他需要你命名的类。 不能声明一个匿名类来实现多个接口,或者继承一个类并同时实现一个接口。 匿名类的客户端不能调用除父类型继承的成员以外的任何成员。 因为匿名类在表达式中出现,所以它们必须保持短——约十行或更少——否则可读性将受损。

在将lambda表达式添加到Java(第6章)之前,匿名类是创建小方法对象和处理对象的首选方法,但lambda表达式现在是首选(条目 42)。 匿名类的另一个常见用途是实现静态工厂方法(请参阅条目 20中的intArrayAsList)。

局部类是四种嵌套类中使用最少的。 一个局部类可以在任何可以声明局部变量的地方声明,并遵守相同的作用域规则。 局部类与其他类型的嵌套类具有共同的属性。 像成员类一样,他们有名字,可以重复使用。 就像匿名类一样,只有在非静态上下文中定义它们时,它们才会包含实例,并且它们不能包含静态成员。 像匿名类一样,应该保持简短,以免损害可读性。

回顾一下,有四种不同的嵌套类,每个都有它的用途。 如果一个嵌套的类需要在一个方法之外可见,或者太长而不能很好地适应一个方法,使用一个成员类。 如果一个成员类的每个实例都需要一个对其宿主实例的引用,使其成为非静态的; 否则,使其静态。 假设这个类属于一个方法内部,如果你只需要从一个地方创建实例,并且存在一个预置类型来说明这个类的特征,那么把它作为一个匿名类; 否则,把它变成局部类。

相关推荐