转载时请以超链接形式标明文章原始出处和作者信息及本声明
http://dwhstudying.blogbus.com/logs/1599885.html
今天看OOD启思录的时候,看到继承那章,回想到之前看Effective JAVA的时候,看到那里举的一个继承的危险性的例子,不过记得不是很精确,于是就翻出那本书:
第十四条,复合优先于继承。
那里说,与调用类的方法不同,继承打破了封装性[Snyder86]。换句话说,一个子类依赖于其超类中特定功能的实现细节。而超类的实现有可能会随着发行版本的不同而有所变化,如果真的发生了变化则子类可能会被打破,即使它的代码完全没有改变。一次一个子类必须要跟着其超类的更新而发展,除非超类是专门为了扩展而被设计的,并且具有很好的文档说明。
举个这样的例子:
public class InstrumentedHashSet extends HashSet {
private int addCount = 0;
........// constructors.
public boolean add( Object o ) {
addCount++;
return super.add( o );
}
public boolean addAll( Collection c ) {
addCount += c.size();
return super.addAll( c );
}
public int getAddCount() {
return addCount;
}
}
这个类看起来非常合理,但是它并不能正常工作。假如我们创建了一个实例,并使用addAll方法添加了三个元素:
InstrumentedHashSet s = new InstrumentedHashSet();
s.addAll( Arrays.asList( new String[] {"Snap","Crackle","Pop"} ) );
这时候,我们一般会期望getAddCount方法会返回3,但是它实际上返回6。错就出在子类没有考虑到HashSet内部的实现细节。HashSet的addAll方法是基于它的add方法来实现的,InstrumentedHashSet中的addAll方法首先给addCount增加3,然后通过super.allAll来调用HashSet的addAll实现。然后又顺次调用到被InstrumentedHashSet改写了的add方法,每个元素调用一次。这三次调用又分别给addCount多加了1,所以,总共增加了6:通过addAll方法增加的每个元素都被计算了两次。
-
在清楚超类的实现细节的话,可以通过修正addAll方法来使子类正确运行。可是,它的功能正确性需要依赖于超类的实现细节。这是一个“Self-use”的例子。而超类的这些实现细节并不是承诺,不能保证不会随着版本不同而不发生变化。所以这样得来的子类将是非常脆弱的。
所以,虽然继承机制的功能非常强大,但是它存在诸多问题。它违背了封装原则。
因此,Effective JAVA给出第十五条:要么专门为继承而继承,并给出文档说明,要么禁止继承。
很多时候可以用复合的方法避免继承。不再是扩展一个已有的类,而是在新的类中增加一个私有域,它引用了这个已有类的一个实例。新类的每一个实例方法都可以调用被包含的已有类的实例中对应的方法,并返回它的结果。这被称为转发(forwarding)。
public class InstrumentedSet implements Set {
private final Set s;
private int addCount = 0;
public InstrumentedSet( Set s ){
this.s = s;
}
........// Forwarding methods
}
通过复合和装发,每一个InstrumentedSet实例都把另一个set实例包装起来,所以这样的类可以被称作包装类(Wrapper class)。这也正是Decorator模式,因为InstrumentedSet类对一个集合进行了修饰,为它增加了计数特性。