第四章 类和接口
第15条 使类和成员的可访问性最小化
-
信息隐藏/封装:区分一个组件设计的好不好,唯一重要的因素在于,它对于外部的其他组件而言,是否隐藏了其内部数据和其他实现细节。
-
规则:尽可能地使每个类或者成员不被外界访问。
-
公有类是包API的一部分,包级私有的顶层类则已经是这个包的实现的一部分。
-
公有类的实例域决不能是公有的。包含公有可变域的类通常并不是线程安全的。
-
长度非零的数组总是可变的,所以让类具有公有的静态final数组域,或者返回这种域的访问方法,这是错误的。(安全漏洞的常见根源)
-
总而言之,应该始终尽可能(合理)地降低程序元素的可访问性。
//Potential security hole! 潜在的安全漏洞
public static final Thing[] VALUES = {...};
//修正方法1:
private static final Thing[] PRIVATE_VALUES = {...};
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
//修正方法2:
private static final Thing[] PRIVATE_VALUES = {...}
public static final Thing[] value() {
return PRIVATE_VALUES.clone();
}
第16条 要在公有类而非公有域中使用访问方法
-
如果类可以在它所在的包之外进行访问,就提供访问方法,以保留将来改变该类的内部表示法的灵活性。
-
如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域并没有本质的错误。
-
简而言之,公有类永远都不应该暴露可变的域。
第17条 使可变性最小化
-
不可变类是指其实例不能被修改的类。
-
使类成为不可变类,要遵循下面5条规则:
-
不要提供任何会修改对象状态的方法(也称为设值方法)
-
保证类不会被扩展:一般做法是声明这个类成为final的
-
声明所有的域都是final的
-
声明所有的域都为私有的
-
确保对于任何可变组件的互斥访问:如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。在构造器、访问方法和readObject方法中请使用保护性拷贝技术。
-
不可变对象本质上是线程安全的,它们不要求同步,可以被自由地共享。
-
不可变类可以提供一些静态工厂,它们把频繁被请求的实例缓存起来,从而当现有实例可以符合请求的时候,就不必创建新的实例。
-
不需要,也不应该为不可变类提供clone方法或者拷贝构造器。
-
不仅可以共享不可变对象,甚至也可以共享它们的内部信息。
-
不可变对象为其他对象提供了大量的构件,无论是可变的还是不可变的对象:这条原则的一种特例在于,不可变对象构成了大量的映射建和集合元素。
-
不可变对象无偿地提供了失败的原子性。
-
不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。创建这些对象的代价可能很高,特别是大型的对象。
-
如果能够精确地预测出客户端将要在不可变的类上执行哪些复杂的多阶段操作,这种包级私有的可变配套类的方法就可以工作得很好。如果无法预测,最好的方法是提供一个公有的可变配套类。例如String的配套StringBuilder。
-
不可变的类变成final的另一种方法就是,让类的所有构造器都变成私有的或者包级私有的,并且添加公有的静态工厂来代替公有的构造器。这种方法还使得有可能通过改善静态工厂的对象缓存能力,在后续的发行版本中改进该类的性能。
-
不可变类:没有一个方法能够对对象的状态产生外部可见的改变,然后许多不可变类拥有一个或者多个非final的域,它们在第一次被请求执行这些计算的时候,把一些开销昂贵的计算结果缓存在这些域中。
-
如果你选择让自己的不可变类实现Serializable接口,并且它包含一个或多个指向可变对象的域,就必须提供一个显示的readObject或者readResolve方法,或者使用ObjectOutputStream.writeUnshared和ObjectInputStream.readUnshared方法,即便默认的序列化形式是可以接受的,也是如此。否则,攻击者可能从不可变的类创建可变的实例。
-
除非有很好的理由要让类成为可变的类,否则它就应该是不可变的。
-
如果类不能被做成不可变的,仍然应该尽可能地显示它的可变性。
-
除非有令人信服的理由要使域变成是非final的,否则要使每个域都是private final的。
-
构造器应该创建完全初始化的对象,并建立起所有的约束关系。不要在构造器或者静态工厂之外再提供公有的初始化方法,除非有令人信服的理由必须这么做。同样的,也不应该提供“重新初始化”方法。
public 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);
}
}
第18条 复合优先于继承
-
在包的内部实现继承是十分安全的,在那里子类和超类的实现都处在同一个程序员控制之下。
-
与方法调用不同的是,继承打破了封装性。换句话说,子类依赖于其超类中特定功能的实现细节。超类的实现有可能会随着发行版本的不同而有所变化,如果真的发生了变化,子类可能会遭到破坏。它们的超类在后续的发行版本中可以获得新的方法。
-
有一种方法可以避免前面的问题:不扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例。这种设计被称为“复合”。(Decorator 修饰者模式)
-
包装类几乎没有什么缺点,需要注意的一点是,包装类不适合用于回调框架:在回调框架中,对象把自身的引用传递给其他的对象,用于后续的调用(“回调”)
-
只有当子类真正是超类的子类型时,才适合用继承。换句话说,对于两个类A和B,只有当两者之间确实存在“is-a”关系的时候,类B才应该扩展类A。
-
继承机制会把超类API中的所有缺陷传播到子类中,而复合则允许设计新的API来隐藏这些缺陷。
-
简而言之,继承的功能非常强大,但是也存在诸多问题,因为它违背了封装原则。
第19条 要么设计继承并提供文档说明,要么禁止继承
-
对于专门为了继承而设计并且具有良好文档说明的类来说:这个类会有一些实质性的限制。
-
首先,该类的文档必须精确地覆盖每个方法所带来的影响。换句话说,该类必须有文档说明它可覆盖的方法的自用性。
-
类必须以精心挑选的受保护的方法的形式,提供适当的钩子(hook),以便进入其内部工作中。
-
对于为了继承而设计的类,唯一的测试方法就是编写子类。经验表明,3个子类通常就足以测试一个可扩展类。
-
为了继承而设计并有可能被广泛使用的类时,必须意识到,对于文档中声明的自用模式,你实际上已经做出了永久的承诺。因此必须在发布类之前编写子类对类进行测试。
-
构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用。(超类的构造器在子类的构造器之前运行,如果子类覆盖方法,将会在子类构造器运行之前先被调用)
-
无论是clone还是readObject,都不可以调用可覆盖的方法,不管是以直接还是间接的方式。
-
若你决定在一个为了继承而设计的类中实现Serializable接口,并且该类有一个readResolve或者writeReplace方法,就必须使readResolve或者writeReplace成为受保护的方法,而不是私有的方法。
-
好的API文档应该描述一个给定的方法,做了什么工作,而不是描述它是如何做到的。
-
对于那些并非为了安全进行子类化而设计和编写文档的类,要禁止子类化:一种方法是声明类为final的;另一种方法是把所有的构造器都变成私有的,或者包级私有的,并增加一些公有的静态工厂来替代构造器。
-
完全消除这个类中可覆盖方法的自用特性,就可以创建“能够安全地进行子类化”的类。
-
简而言之,专门为了继承而设计类是一件很辛苦的工作。除非知道真正需要子类,否则最好通过将类声明为final,或者确保没有可访问的构造器来禁止类被继承。
第20条 接口优于抽象类
-
接口、抽象类都可以用来定义允许多个实现的类型。区别在于抽象类,类必须成为抽象类的一个子类。
-
现有的类可以很容易被更新,以实现新的接口。
-
接口是定义mixin(混合类型)的理想选择。
-
接口允许构造非层次结构的类型框架。
-
通过第18条的包装类模式,接口使得安全地增强类的功能成为可能。
-
通过对接口提供一个抽象的骨架实现类,可以把接口和抽象类的优点结合起来。接口负责定义类型,或者还提供一些缺省方法,而骨架实现类则负责实现除基本类型接口方法之外,剩下的非基本类型接口方法。扩展骨架实现占了实现接口之外的大部分工作。这就是模板方法模式。
-
骨架实现类的美妙之处在于,他们为抽象类提供了实现上的帮助,但又不强加“抽象类被用作类型定义时”所持有的严格限制。
-
骨架实现类的编写:确定哪些方法是最为基本的,其他的方法则可以根据它们来实现。这些基本方法将成为骨架实现类中的抽象方法;在接口中未所有可以在基本方法之上直接实现的方法提供缺省方法,但要记住,不能为Object方法提供缺省方法。
-
骨架实现类是为了继承的目的而设计的,对于骨架实现类而言,好的文档绝对是非常必要的。
-
总而言之,接口通常是定义允许多个实现的类型的最佳途径。如果你导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类。而且,还应该尽可能地通过缺省方法在接口中提供骨架实现,以便接口的所有实现类都能使用。
第21条 为后代设计接口
-
尽管缺省方法现在已经是Java平台的组成部分,但谨慎设计接口仍然是至关重要的。
-
在发布程序之前,测试每一个新的接口就显得尤其重要。程序员应该以不同的方法实现每一个接口,最起码不应少于三种实现。
第22条 接口只用于定义类型
-
类实现接口,接口就充当可以应用这个类的实例的类型。为了任何其他目的而定义接口是不恰当的。
-
有一种接口被称为常量接口,不满足上述条件。
-
常量接口模式是对接口的不良使用。
-
常量的几种方案:
-
与某个类或接口紧密相关,就应该把常量添加到类或接口中,如Integer.MAXVALUE与MINVALUE
-
如果常量最好被看做枚举类型的成员,就应该用枚举类型
-
不可实例化的工具类:如果大量利用工具类导出的常量,可以通过利用静态导入机制,避免用类名来修饰常量名。
第23条 类层次优于标签类
-
标签类:例如一个类根据某个字段决定表示一个圆还是一个矩形。
-
标签类过于冗长,容易出错,并且效率低下。
-
子类型化:能表示多种风格对象的单个数据类型。
第24条 静态成员类优于非静态成员类
-
嵌套类是指定义在另一个类内部的类。
-
嵌套类有四种:静态成员类,非静态成员类,匿名类,局部类。除了第一种外,其他三种都称为内部类。
-
静态成员类是外围类的一个静态成员,可以访问外围类的所有成员,包括那些声明为私有的成员。静态成员类是外围类的一个静态成员,与其他的静态成员一样,也遵守同样的可访问性规则。
-
静态成员类的一种常见用法是作为公有的辅助类,只有与外部类一起使用才有意义。
-
语法上,静态成员类与非静态成员类只差一个static。
-
非静态成员的每个实例都隐含地与外围类的一个外围实例相关联。
-
如果声明成员类不要求访问外围实例,就要始终把修饰符static放在它的声明中,使它成为静态成员类。
-
私有静态成员类的一种常见用法是代表外围类所代表对象的组件。
-
匿名类是在使用时同时被声明和实例化。匿名类必须保持简短(大约10行或更少)。否则会影响程序的可读性。
-
在Java中增加lambda之前,匿名类是动态地创建小型函数对象和过程对象的最佳方式。
-
局部类很少使用,在任何“可以声明局部变量”的地方,都可以声明局部类。必须非常简短。局部类有名字。
-
总而言之,如果一个嵌套类需要在单个方法之外仍然可见,就应该使用成员类;如果成员类的每个实例都需要一个指向其外围实例的引用,就要把成员类做成非静态的;否则,就做成静态的。假设这个嵌套类属于一个方法内部,如果你只需要在一个地方创建实例,并且已经有一个预置的类型可以说明这个类的特征,就把它做成匿名类,否则,就做成局部类。
第25条 限制源文件为单个顶级类
-
一个文件中只定义一个顶级类
-
永远不要把多个顶级类或者接口放在一个源文件中