阅读经典——《Effective Java》07
继承(inheritance)是实现代码重用的有力手段,但并非总是最好的选择。继承打破了封装性,因为子类依赖于超类中特定功能的实现细节。超类的实现有可能随着发行版本的不同而有所变化,导致子类遭到破坏。
- 子类遭到破坏的案例
- 使用复合和转发
子类遭到破坏的案例
假设有一个程序使用HashSet,为了查看它自创建以来曾经添加过多少个元素,我们可以通过继承扩展HashSet,重写add和addAll方法。
public class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;
public InstrumentedHashSet() {}
public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override
public boolean add(E e) {
addCount ++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
这段代码看上去没什么问题,假如执行下面的程序,我们期望getAddCount
返回3,但它实际上返回的是6。
InstrumentedHashSet<String> s = new InstrumentedHashSet<String>();
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
System.out.println(s.getAddCount());
哪里出错了?
在HashSet内部,addAll
方法是基于add
方法来实现的,即使HashSet的文档中并没有说明这一细节,这也是合理的。因此InstrumentedHashSet中的addAll
方法首先把addCount
增加了3,然后利用super.addAll()
调用HashSet的addAll实现,在该实现中又调用了被InstrumentedHashSet覆盖了的add
方法,每个元素调用一次,这三次又分别给addCount
增加了1,所以总共增加了6。
因此,使用继承扩展一个类很危险,父类的具体实现很容易影响子类的正确性。而复合优先于继承告诉我们,不用扩展现有的类,而是在新类中增加一个私有域,让它引用现有类的一个实例。这种设计称为复合(Composition)。
使用复合和转发
使用复合来扩展一个类需要实现两部分:新的类和可重用的转发类。转发类用于将所有方法调用转发给私有域。这样得到的类非常稳固,不依赖于现有类的实现细节。请看下面的例子。
//Wrapper class - use composition in place of inheritance
public class InstrumentedSet<E> extends ForwardingSet<E>{
private int addCount = 0;
public InstrumentedSet(Set<E> s) {
super(s);
}
@Override
public boolean add(E e) {
addCount ++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
//Reusable forwarding class
class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) {this.s = s;}
@Override
public int size() {return s.size();}
@Override
public boolean isEmpty() {return s.isEmpty();}
@Override
public boolean contains(Object o) {return s.contains(o);}
@Override
public Iterator<E> iterator() {return s.iterator();}
@Override
public Object[] toArray() {return s.toArray();}
@Override
public <T> T[] toArray(T[] a) {return s.toArray(a);}
@Override
public boolean add(E e) {return s.add(e);}
@Override
public boolean remove(Object o) {return s.remove(o);}
@Override
public boolean containsAll(Collection<?> c) {return s.containsAll(c);}
@Override
public boolean addAll(Collection<? extends E> c) {return s.addAll(c);}
@Override
public boolean retainAll(Collection<?> c) {return s.retainAll(c);}
@Override
public boolean removeAll(Collection<?> c) {return s.retainAll(c);}
@Override
public void clear() {s.clear();}
}
现在,使用InstrumentedSet不会再出上面的问题了,因为无论是add
方法还是addAll
方法都转发给了私有域s
来处理,这些方法对于s
来说总是一致的,不会受InstrumentedSet的影响。另一个好处是此时的包装类InstrumentedSet可以用来包装任何Set实现,有了更广泛的适用性。例如
Set<Date> s = new InstrumentedSet<Date>(new TreeSet<Date>(cmp));
Set<E> s2 = new InstrumentedSet<E>(new HashSet<E>(capacity));
只有当子类和超类之间确实存在父子关系时,才可以考虑使用继承。否则都应该用复合,包装类不仅比子类更加健壮,而且功能也更加强大。
关注作者或文集《Effective Java》,第一时间获取最新发布文章。