第4章 类和接口
13.使类和成员的可访问性最小化
信息隐藏information hiding 或 封装 encapsulation
尽可能地使每个类或者成员不被外界访问。
对于顶层的(非嵌套的)类和接口,只有两种可能的访问级别:包级私有的package-private和公有的public。
对于成员(域、方法、嵌套类和嵌套接口)有四种可能的访问级别,下面按照可访问性的递增顺序罗列出来:
- 私有的private,只有在声明该成员的顶层类内部才可以访问这个成员
- 包级私有的package-private,声明该成员的包内部的任何类都可以访问这个成员。从技术上讲,它被称为缺省default访问级别,如果没有为成员指定访问修饰符,就采用这个访问级别。
- 受保护的protected,声明该成员的类的子类可以访问这个成员,但有一些限制,并且,声明该成员的包内部的任何类也可以访问该成员。不同package的子类继承也不能访问父类的protected成员
- 公有的public,在任何地方都可以访问该成员。
如果方法覆盖了超类中的一个方法,子类中的访问级别就不允许低于超类中的访问类别。这样可以确保任何可使用超类的实例的地方也都可以使用子类的实例。这条规则有种特殊的情形:如果一个类实现了一个接口,那么接口中的所有的类方法在这个类中也都必须被声明为公有的。之所以如此,是因为接口中的所有方法都隐含着公有访问级别。
实例域决不能是公有的。包含公有可变域的类并不是线程安全的。同样的建议也适用于静态域。
长度非零的数组总是可变的,所以,类具有公有的静态final数组域,或者返回这种域的访问方法,这几乎总是错误的。可以使公有数组变成私有的,并增加一个公有的不可变列表:
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
另一种方法是,可以使数组变成私有的,并添加一个公有方法,它返回私有数组的一个备份:
private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {
return PRIVATE_VALUES.clone();
}
14.在公有类中使用访问方法而非公有域
如果类可以在它所在的包的外部进行访问,就提供访问方法,以保留将来改变该类的内部表示法的灵活性。
15.使可变性最小化
为了使类称为不可变,要遵循下面五条规则:
- 不要提供任何会修改对象状态的方法
- 保证类不会被扩展
- 使所有的域都是final的
- 使所有的域都成为私有的
- 确保对于任何可变组件的互斥访问。如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法中返回该对象引用。在构造器、访问方法和readObject方法中请使用保护性拷贝技术。
不可变对象本质上是线程安全的,它们不要求同步。不可变对象可以被自由地共享。
不可变对象可以被自由地共享导致的后果是,永远也不需要进行保护性拷贝。
不仅可以共享不可变对象,甚至也可以共享它们的内部信息。
不可变对象为其他对象提供了大量的构件。不可变对象构成了大量的map key和set element。
不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。
如果你选择让自己的不可变类实现Serializable
接口,并且它包含一个或者多个指向可变对象的域,就必须提供一个显式的readObject
或者readResolve
方法,或者使用ObjectOutputStream.writeUnshareed
和ObjectInputStream.readUnshared
方法,即使默认的序列化是可以接受的,也是如此,否则攻击装可能从不可变的类创建可变的实例。
16.复合优于继承
适用于实现继承,当一个类扩展另一个类时。不适用于接口继承,当一个类实现一个接口的时候,或者当一个接口扩展另一个接口的时候。
不用扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例。这种设计被称作复合,因为现有的类变成了新类的一个组件。新类中的每个实例方法都可以调用被包含的现有类实例中的对应的方法,并返回它的结果,这被称为转发,新类中的方法被称为转发方法。注意这个实现分为两部分:类本身和可重用的转发类,包含了所有的转发方法,没有其他方法。
包装类,Decorator模式。
包装类几乎没有什么缺点,需要注意的一点是,包装类不适合用在回调框架中;在回调框架中,对象把自身的引用传递给其他的对象,用于后续的调用。因为被包装起来的对象并不知道它外面的包装对象,所以它传递一个指向自身的引用this,回调时避开了外面的包装对象。这被称为SELF问题。
只有当子类真正是超类的子类型时,才适合用继承。换句话说,对于两个类型A和B,只有当两者之间确实存在is-a
关系的时候,类B才应该扩展类A。如果子类和超类处在不同的包中,并且超类并不是为了继承而设计时,那么继承将会导致脆弱性。为了避免这种脆弱性,可以用复合和转发机制来代替继承,尤其是当存在合适的接口可以实现包装类时。包装类不仅比子类更加健壮,而且功能也更加强大。
17.要么为继承而设计,并提供文档说明,要么就禁止继承
对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化。有两种方法可以禁止子类化,比较容易的办法是把这个类声明为final的,另一种办法是把所有的构造器都变成私有的,或者包级私有的,并增加一些公有的静态工厂来替换构造器。
18.接口优于抽象类
Java提供了两种机制,可以用来定义允许多个实现的类型:接口和抽象类。这两种机制之间最明显的区别在于,抽象类允许包含某些方法的实现,但是接口则不允许。另一个更重要的区别在于,Java只允许单继承,但允许多实现。
现有的类可以很容易被更新,以实现新的接口。
接口是定义mixin混合类型的理想选择。
接口允许我们构造非层次结构的类型框架。
包装类模式,接口使得安全地增强类的功能称为可能。
虽然接口不允许包含方法的实现,但是,使用接口来定义类型并不妨碍你为程序员提供实现上的帮助。通过对你导出的每个重要接口都提供一个抽象的骨架实现类,把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但是骨架实现类接管了所有与接口实现相关的工作。
按照惯例,骨架实现被称为AbstractInterface
,这里的Interface
是指所实现的接口的名字。例如,Collections Framework为每个重要的集合接口都提供了一个骨架实现,包括AbstractCollection
,AbstractSet
,AbstractList
和AbstractMap
。
19.接口只用于定义类型
常量接口没有包含任何方法,它只博涵静态的fianl域,每个域都导出一个常量。常量接口模式是对接口的不良使用。
如果要导出常量,可以由几种合理的选择方案。如果这些常量与某个现有的类火灾接口紧密相关,就应该吧这些常量添加到这个类或者接口中。例如Integer.MAX_VALUE
。如果这些常量最好被看作枚举类型的成员,就应该使用枚举类型来导出这些常量。否则,应该使用不可实例化的工具类来导出这些常量。如果大量利用工具类导出的常量,可以通过利用静态导入机制避免用类名来修饰常量名。
20.类层次优于标签类
21.用函数对象表示策略
有些语言支持函数指针function pointer、代理delegate、lambda表达式,或者支持类似的机制,允许程序把调用特殊函数的能力存储起来并传递这种能力。这种机制常用于允许函数的调用者通过传入第二个函数,来指定自己的行为。
Java没有提供函数指针,但是可以用对象引用实现同样的功能。
String.CASE_INSENSITIVE_ORDER
函数指针的主要用途就是实现策略模式。为了在Java中实现这种模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体策略只被使用一次时,通常使用匿名类来声明和实例化这个具体策略类。当一个具体策略是设计用来重复使用的时候,它的类通常就要被实现为私有的静态成员类,并通过公有的静态fianl域被导出,其类型为该策略接口。
22.优先考虑静态成员类
嵌套类是指定义在另一个类的内部的类。嵌套类有四种:静态成员类、非静态成员类、匿名类、局部类。
非静态成员类的每个实例都隐含着与外围类的一个外围实例相关联。在非静态成员类的实例方法内部,可以调用外围实例上的方法,或者利用修饰过的this构造获得外围实例的引用。如果嵌套类的实例可以在它外围类的实例之外独立存在,这个嵌套类就必须是静态成员类,在没有外围实例的情况下,要想创建非静态成员类的实例是不可能的。