《Effective Java》读书笔记
继承可能导致的问题
首先,大部分情况下,包的内部继承是安全的,因为子类和超类都是一个程序员控制。
对普通的具体类(concrete class)进行跨越包边界的继承,则是非常危险的。
原因在于,子类依赖于其超类中特定功能的实现细节,超类的实现有可能随着发行版本的不同而有所变化,如果真的发生变化,子类可能遭到破坏。
举个例子。
实现一个记录集合中曾经添加过多少个元素的HashSet. HashSet包含了两个可以添加元素的方法:add 和 addAll,因此这两个方法都要被覆盖:
假设我们创建了一个实例,并使用addAll方法添加了三个元素:
此刻,我们希望getAllCount方法返回3,但是它实际上返回的是6。
原因在于,HashSet的addAll方法是基于add方法实现的。
我们去调被覆盖的addAll方法,可以解决这个问题, 但是如果有一天,addAll不是基于add实现的呢?
HashSet的addAll方法是基于add方法实现的,这种自用性(self-use)是实现细节,不能保证在所有实现中都保持不变,因此依赖于此的子类将是非常危险的。
还有一个办法:覆盖addAll方法,保证实现方式是遍历调用add。这个方法的问题有两个:首先,这相当于重新实现了超类的方法,很困难,耗时,容易出错;其次,由于覆盖的时候无法访问对于子类来说的私有域,所以有些方法无法实现。
还有一种导致子类的脆弱的原因:超类在后续发行版本中可能添加新的方法。举个例子,一个子类覆盖了超类的所有添加元素的方法,以保证在添加元素的时候对元素的条件进行判断。一旦超类增加了一个新的添加元素的方法,就有可能导致非法的元素添加到实例中,造成问题。
如果不覆盖方法,只是添加新的方法的继承,也会出问题吗?
如果子类添加了一个新方法,而超类在后续的版本中液体阿加了一个签名相同但是返回值不同的方法,这样的子类将无法通过编译。
继承的替代方案:复合
有一种方法可以避免前面提到的所有问题:放弃继承,而是在新的类中增加一个私有域,它引用现有类的一个实例。这种设计被称为“复合(composition)”。新类中的每个方法都可以调用被包含的现有实例中对应的方法,这种方式称为转发(forwarding),新类中的方法称为转发方法(forwarding method)。
举个例子:
这个解决方式依赖于Set接口的存在。这样的包装类(wrapper class)可以用来包装任何set实现:
还可以临时替换一个继承了同样接口的实例:
包装类不适合用在回调框架(callback framework) ,即对象把自身的引用传递给其他对象,用于后续回调。这时,传递出去的引用因为避开了包装类,可能会导致问题。
如果硬要继承,需要注意什么
如果确认子类真正是超类的子类型时,才适合用继承。
首先,该类的文档必须精确的描述覆盖每个方法所带来的影响,即说明它可覆盖(overridable)的方法的自用型(self-use)。对于每个公有的或者受保护的方法或者构造器,它的文档必须指明它是什么时候,按照什么顺序,调用了哪些可覆盖的方法。
同时,编写子类进行测试,暴露遗漏的问题。
还有很重要的一点:构造器绝不能调用可被覆盖的方法。由于超类的构造器必然在子类构造器之前运行,子类中覆盖版本的方法可能在子类构造器运行之前就被调用,引发问题,比如访问未被初始化的子类变量等。同样的限制也适用于clone和readObject方法。
为了避免继承导致的问题,还有一个解决方案:
读于那些并非为了安全的进行子类化而设计和编写文档的类,要禁止子类化。
可以通过以下方法来达到禁止子类化的目的:
- 声明该类为final
- 把所有构造器都变为私有,使用公有的静态工厂来替代构造器。
如果非要继承,该类有没有实现标准的接口,一种合理的办法是:确保这个类永远不会调用它的任何可覆盖的方法,完全消除这个类中可覆盖方法的自用特性。
具体做法可以是将每个可覆盖方法的代码体移到一个私有的辅助方法(helper method)中,并且让这个可覆盖的方法调用它的私有辅助方法。然后,在其他方法中,调用这个可覆盖方法的私有辅助方法,来代替可覆盖方法的每个自用调用。
网友评论