第十五条:使类和成员的可访问性最小化
封装(信息隐藏):隐藏所有的实现细节,把API与实现清晰的隔离开来,然后组件之间只通过API进行通信,一个模块不需要知道其他模块的内部工作情况。
封装的原因:解耦。
尽可能的使每个类或者成员不被外界访问。
公有类的实例域决不能是公有的。
包含公有可变域的类通常不是线程安全的。
让类具有公有的静态final数组域或者返回这种域的访问方法,这是错误的。
修正这个问题的方法:使公有数组变成私有的,并增加一个公有的不可变列表,或者也可以使数组变成私有的,并添加一个公有方法,它返回私有数组的一个拷贝。
Java 9 模块系统。
总而言之,应该尽可能的降低程序元素的可访问性,除了公有静态final域的特殊情况之外(此时他们充当常量),公有类都不应该包含公有域,并且要确保公有静态final域引用的对象都是不可变的。
第十六条:要在公有类而非公有域中使用访问方法
如果类可以在它所在的包之外进行访问,就提供访问方法,以保留将来改变该类的内部表示法的灵活性。
如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域并没有本质错误。
简而言之,公有类永远都不应该暴露可变的域,虽然还是有问题,但是让公有类暴露不可变的域,其危害相对较小,但有时候会需要用包级私有的或者私有的嵌套类来暴露域,无论这个类是可变的还是不可变的。
第十七条:使可变性最小化
为了使类成为不可变的,要遵循五条规则:
不要提供会修改对象状态的方法。
保证类不会被扩展。
声明所有的域都是final。
声明所有的域都是私有的。
确保对于任何可变组件的互斥访问。
不可变对象本质上是线程安全的,他们不要求同步。
不可变对象的优点:不仅可以共享不可变对象,甚至可以共享他们的内部信息。不可变对象为其他对象提供了大量的构件。不可变对象无偿的提供了失败的原子性。
缺点:对于每个不同的值都需要一个单独的对象,这可能会花费很大的代价,牺牲性能。
除非有很好的理由要让类成为可变的类,否则他就应该是不可变的。
如果类不能被做成不可变的,仍然应该尽可能的限制它的可变性。
除非有令人信服的理由要使域变为非final的,否则要使每个域都是private final的。
构造器应该创建完全初始化的对象,并建立起所有的约束关系。
第十八条:复合优先于继承
与方法调用不同,继承打破了封装性。
不扩展现有的类,而是在新的类中加入一个私有域,它引用现有类的一个实例,这种设计称之为“复合”。因为现有的类变成了新类的一个组件。新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回他的结果,这被称为转发,新类中的方法被称为转发方法。
每一个InstrumentSet实例都把另一个Set实例包装起来了,所以InstrumentSet类被称为包装类,这正是修饰者模式。
有时转发和复合的结合也被宽松的称为委托。但从技术角度而言,这不是委托,除非包装对象把自身传递给被包装的对象。
包装类基本上没有什么缺点,但需要注意,包装类不适合于回调框架。
简而言之,继承的功能很强大,但是也存在很多问题,因为他违背了封装原则,只有当子类和超类之间确实存在子类型关系的时候,使用继承才是恰当的。即便如此,如果子类和超类处在不同的包里,并且超类并不是为了继承才设计的,那么继承将会导致脆弱性,为了避免这种脆弱性,可以用复合和转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候。包装类不仅比子类更健壮,而且功能也更强大。
第十九条:要么设计继承并提供文档说明,要么禁止继承
该类必须有文档说明他可覆盖的方法的自用性。
对于为了继承而设计的类,唯一的测试方法就是编写子类测试。而且必须在发布类之前先编写子类对类进行测试。
构造器绝不能调用可被覆盖的方法,无论直接调用还是间接调用。
无论是clone还是readObject,都不可以调用可覆盖的方法,不管是以直接还是间接的方式。
对于那些并非为了安全的进行子类化而设计和编写文档的类,要禁止子类化。
简而言之,专门为了继承而设计类是一件很辛苦的工作。你必须建立文档说明其所有自用模式,并且一定那建立文档,在这个类的生命周期中就必须遵守。如果没有做到,子类就会依赖超类的实现细节,如果超类的实现发生了变化,他就有可能遭到破坏。为了允许其他人能编写出高效的子类,你还必须导出一个或者多个受保护的方法。除非知道真正需要子类,否则最好通过将类声明为final,或者确保没有可访问的构造器来禁止类被继承。
第二十条:接口优于抽象类
接口使得安全的增强类的功能成为可能。如果使用抽象类来定义类型,那么程序员除了使用继承的手段来增加功能,再没有其他选择了。这样得到的类与包装类相比,功能更差,也更加脆弱。
通过对接口提供一个抽象的骨架实现类,可以把接口和抽象类的优点结合起来。接口负责定义类型,或许还提供一些缺省方法,而骨架实现类则负责实现除基本类型接口方法之外,剩下的非基本类型接口方法。扩展骨架实现占了实现接口之外的大部分工作。这就是模板方法模式。
编写骨架实现类的过程。
首先,必须认真研究接口,并确定哪些方法是最为基本的,其他的方法则可以根据他们来实现。这些基本方法将成为骨架实现类中的抽象方法。接下来,在接口中为所有可以在基本方法之上直接实现的方法提供缺省方法,但要记住,不能为Object方法提供缺省方法。如果基本方法和缺省方法覆盖了接口,你的任务就完成了,不需要骨架实现类了。否则,就要编写一个类,声明实现接口,并实现所有剩下的接口方法。这个类包含任何非公有的域,以及适合该任务的任何方法。
总而言之,接口通常是定义允许多个实现的类型的最佳途径。如果你导出一个重要的接口,就应该坚决考虑同时提供骨架实现类。而且,还应该尽可能的通过缺省方法在接口中提供骨架实现,以便接口的所有实现类都能使用。也就是说,对于接口的限制,通常也限制了骨架实现会采用的抽象类的形式。
第二十一条:为后代设计接口
有了缺省方法,接口的现有实现就不会出现编译时没有报错或警报,运行时却失败的情况,但虽然如此,谨慎设计接口仍然是至关重要的。
第二十二条:接口只用于定义类型
常量接口模式是对接口的不良使用。
如果要导出常量,可以有几种合理的选择方案。如果这些常量与某个现有的类或者接口紧密相关,就应该把这些常量添加到这个类或者接口中。如果这些常量最好被看作枚举类型的成员,就应该用枚举类型来导出。否则,应该使用不可实例化的工具类来导出。
如果其中包含5个或者5个以上连续的数字,无论是浮点还是定点,都要考虑在数字的字面量中添加下划线。对于基数为10的字面量,无论是整数还是浮点,都应该用下划线把数字隔成每三位一组,表示一千的正负倍数。
接口应该只被用来定义类型,不应该被用来导出常量。
第二十三条:类层次优于标签类
标签类过于冗长,容易出错,并且效率低下。
类层次代码简单且清楚,不包含在原来版本中见到的所有样板代码。每个类型的实现都配有自己的类,这些类都没有收到不相关数据域的拖累。所有域都是final的。编译器确保每个类的构造器都初始化他的数据域,对于根类中声明的每个抽象方法都确保有一个实现。这样就杜绝了由于遗漏switch case而导致运行时失败的可能性。多名程序员可以独立的扩展层次结构,并且不用访问根类的源代码就能互相操作。每种类型都有一种相关的独立的数据类型,允许程序员指明变量的类型,限制变量,并将参数输入到特殊的类型。
类层次的另一个好处在于,他们可以用来反应类型之间本质上的层次关系,有助于增强灵活性,并有助于更好的进行编译时类型检查。
简而言之,标签类很少有适用的时候。当你想要编写一个包含显式标签域的类时,应该考虑一下,这个标签是否可以取消,这个类是否可以用类层次来代替。当你遇到一个包含标签的现有类时,就要考虑将他重构到一个层次结构中去。
第二十四条:静态成员类优于非静态成员类
嵌套类有四种:静态成员类、非静态成员类、匿名类、局部类。
静态成员类是外围类的一个静态成员,能访问外围类的所有成员。
静态成员类的一种常见的用法是作为公有的辅助类,只有与他的外部类一起使用才有意义。
非静态成员类的每个实例都隐含的与外围类的一个外围实例相关联。在非静态成员类的实例方法内部,可以调用外围实例上的方法,或者利用修饰过的this构造获得外围实例的引用。如果嵌套类的实例可以在他外围类的实例之外独立存在,这个嵌套类就必须是静态成员类:在没有外围实例的情况下,要想创建非静态成员类的实例是不可能的。
当非静态成员类的实例被创建的时候,他和外围实例之间的关联关系也随之被建立起来,而且,这种关联关系以后不能被修改。通常情况下,当在外围类的某个实例方法的内部调用非静态成员类的构造器时,这种关联关系被自动建立起来。
非静态成员类的一种常见用法是定义一个Adapter,让她允许外部类的实例被看作是另一个不相关的类的实例。
如果声明成员类不要求访问外围实例,就要始终把修饰符static放在他的声明中。
总而言之,如果一个嵌套类需要在单个方法之外仍然是可见的,或者他太长了,不适合放在方法内部,就应该使用成员类。如果成员类的每个实例都需要一个指向其外围实例的引用,就要把成员类做成非静态的,否则就做成静态的。假设这个嵌套类属于一个方法的内部,如果你只需要在一个地方创建实例,并且已经有了一个预置的类型可以说明这个类的特征,就要把他做成匿名类,否则就做成局部类。
第二十五条:限制源文件为单个顶级类
一个源文件定义多个顶级类可能导致给一个类提供多个定义,哪一个定义会被用到,取决于源文件被传给编译器的顺序。
如何解决这个问题?
只要把顶级类分别放入独立的源文件即可,如果一定要把多个顶级类放进一个源文件中,就要考虑使用静态成员类,以此代替将者两个类分到独立源文件中去。如果这些类服从于另一个类,那么将他们做成静态成员类比较好,因为这样增强了代码的可读性,如果将这些类声明为私有的,还可以使他们减少被读取的概率。
结论显而易见:永远不要把多个顶级类或者接口放在一个源文件中。遵循这个规则可以确保编译时一个类不会有多个定义。这么做反过来也能确保编译产生的类文件,以及程序结果的行为,都不会收到源文件被传给编译器时的顺序的影响。