16、复合优先于继承
继承的功能非常强大,但它违背了封装原则。只有当子类和超类之间确实存在子类型关系时,使用继承才是恰当的。即便如此,如果子类和超类处在不同的包中,并且超类并不是为了继承而设计的,那么继承会很脆弱(子类必须跟着其超类的更新而演变)。原因在于超类在后续的发行版本中可以获得新的方法,一旦超类增加了新的方法,很可能仅仅调用了这个未被子类覆盖的新方法,将“非法”元素添加到子类实例中。该问题来源于覆盖。
解决办法(用复合):不用扩展现有类,而是在新的类中增加一个私有域,它引用现有类(即之前打算继承超类)的一个实例(这边涉及到转发概念。新类每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果,这被称为转发)。装饰者模式含有这种复合的思想,初始化就像包装类一样。如Set<Data> s = new InstrumentSet<Data>(newTreeSet<Data>(cmp));
注意:包装类不适合用在回调框架中;在回调框架中,对象把自身的引用传递给其他的对象,用于后继的调用(回调),因为被包装起来的对象并不知道它外面的包装对象,所以它传递一个指向自身的引用(this),回调时避开了外面的包装对象。
17、要么为继承而设计,并提供文档说明,要么就禁止继承
为了允许继承,构造器决不能调用可覆盖的方法,无论是直接调用还是间接调用。超类的构造器在子类的构造器之前运行,所以子类中覆盖版本的方法将会在子类的构造器运行之前就先被调用。如果该覆盖版本的方法依赖于子类构造器所执行的任何初始化工作,该方法将不会如预期般地执行。如:
public class Super {
publicSuper(){
overrideMe();
}
public void overrideMe(){}
}
public class Sub extends Super{
private final Date date;
public Sub(){
date = new Date();
}
public void overrideMe(){
System.out.println(date);
}
public static void main(String[] args) {
Sub sub = new Sub();//这边构造时,先调用父类构造,此时date还没初始化,父类构造又调用了被覆盖的overrideMe方法,打印date,导致第一个date输出为null。
sub.overrideMe();
}
}
如果你决定在一个为了继承而设计的类中实现Cloneable或者Serializable接口,就应该意识到,因为clone和readObject方法在行为上非常类似于构造器,因此无论是cline还是readObject,都不可以调用可覆盖的方法,不管是以直接还是间接的方式。对于readObject方法,覆盖版本的方法将在子类的状态被反序列化之前先被运行;而对于clone方法,覆盖版本的方法则是在子类的clone方法在子类被克隆之前先被运行。
禁止子类化方法:(1)把这个类声明为final。(2)把所有的构造器都变成私有的,或者包级私有,并增加一些公有的静态工厂来替代构造器。
必须允许从这样的类继承方法:确保这个类永远不会调用它的任何可覆盖的方法,并在文档中说明这一点。实现方法:把每个可改写的方法的代码体移到一个私有的“辅助方法”中,并且让每个可改写的方法调用他的私有辅助方法,然后用“直接调用可改写方法的私有辅助方法”来代替“可改写方法的每个自用调用”。
18、接口优于抽象类(骨架实现skeletal implementation、模拟多重继承)
骨架实现类是为了继承的目的而设计的,它具有简单实现,但与简单实现不同在于,骨架实现是抽象的。骨架实现类的命名方法为: AbstractInterface,这里的Interface指的是接口的名字。
接口的好处:(1)已有的类可以很容易被更新,以实现新的接口。(2)接口是定义混合类型的理想选择。(3)接口使得我们可以构造出非层次结构的类型框架。层次结构用来描述组织机构这样的事物是非常合适的。但并不是所有事物都适用。
抽象类的一个明显优势:抽象类的演变比接口的演变要容易得多。接口增加了新的方法将会影响对接口实现的类···。当演变的容易性比灵活性和功能更为重要的时候,应该使用抽象类来定义类型。
19、接口只用于定义类型
接口应该只被用来定义类型,它们不应该被用来导出常量。
导出常量的几种合理的选择方案:(1)如果常量与某个现有的类或者接口紧密相关,就应该把常量直接添加到这个类或者接口中;(2)如果这些常量最好被看作枚举类型的成员,就应该用枚举类型来导出这些常量;(3)否则,应该用不可实例化(构造函数设为private)的工具类来导出这些常量,可以通过静态导入机制(import static 包名)。
20、类层次优于标签类
标签类就是那种多种实现一股脑儿挤在一个类中。
21、用函数对象表示策略
函数指针的主要用途就是实现策略模式(正确,可以借助headfirst设计模式的策略模式中鸭子飞行行为和叫声行为案例一起结合理解)。为了在java中实现这种模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类。(1)当一个具体策略只被使用一次时,通常使用匿名类来声明和实例化这个具体策略类。如:Array.sort(stringArray,newComparator<String>(){比较函数的具体实现;});(2)当一个具体策略是设计用来重复使用的时候,它的类通常就要实现为私有的静态成员类,并通过公有的静态final域被导出,其类型为该策略接口。如:
public class Host {
private static class StrLenCmp implementsComparator<String>{
publicint compare(String s1,String s2){
return s1.length()-s2.length();
}
}
public static final Comparator<String>STRING_LENGTH_COMPARATOR = newStrLenCmp();
}
22、优先考虑静态内部类
四种内部类(静态、局部、成员、匿名)统称嵌套类(这与java编程思想中还是有所区别,那边说静态内部类叫嵌套类···)。
非静态成员类(成员内部类)的一种常见用法是定义一个适配器,它允许外部类的实例被看作是另一个不相关的类的实例(适配器模式,可以理解)。如果省略了static修饰符,则每个实例都将包含一个额外的指向外围对象的引用。保存这份引用要消耗时间和空间,并且会导致外围实例在符合垃圾回收时却仍然得以保留。如果没有外围实例,则由于要引用,故不能再使用非静态成员类。
如果嵌套类的实例可以在它外围类的实例之外(不管实例是否存在),这个嵌套类就必须是静态成员类。
私有静态成员类的一种常见用法是用来代表外围类所代表的对象的组件(组件是能够完成某种功能并且向外提供若干个使用这种功能的接口的可重用代码集)。
匿名类的常见用法:(1)动态地创建函数对象,见21;(2)创建过程对象,如Thread;(3)在静态工厂方法的内部。