閱讀經(jīng)典——《Effective Java》07
繼承(inheritance)是實(shí)現(xiàn)代碼重用的有力手段私爷,但并非總是最好的選擇。繼承打破了封裝性伞插,因?yàn)樽宇愐蕾囉诔愔刑囟üδ艿膶?shí)現(xiàn)細(xì)節(jié)号阿。超類的實(shí)現(xiàn)有可能隨著發(fā)行版本的不同而有所變化斟珊,導(dǎo)致子類遭到破壞写妥。
- 子類遭到破壞的案例
- 使用復(fù)合和轉(zhuǎn)發(fā)
子類遭到破壞的案例
假設(shè)有一個程序使用HashSet拳球,為了查看它自創(chuàng)建以來曾經(jīng)添加過多少個元素,我們可以通過繼承擴(kuò)展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;
}
}
這段代碼看上去沒什么問題,假如執(zhí)行下面的程序,我們期望getAddCount
返回3莱找,但它實(shí)際上返回的是6酬姆。
InstrumentedHashSet<String> s = new InstrumentedHashSet<String>();
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
System.out.println(s.getAddCount());
哪里出錯了?
在HashSet內(nèi)部宋距,addAll
方法是基于add
方法來實(shí)現(xiàn)的,即使HashSet的文檔中并沒有說明這一細(xì)節(jié)症脂,這也是合理的谚赎。因此InstrumentedHashSet中的addAll
方法首先把addCount
增加了3,然后利用super.addAll()
調(diào)用HashSet的addAll實(shí)現(xiàn)诱篷,在該實(shí)現(xiàn)中又調(diào)用了被InstrumentedHashSet覆蓋了的add
方法壶唤,每個元素調(diào)用一次,這三次又分別給addCount
增加了1棕所,所以總共增加了6闸盔。
因此,使用繼承擴(kuò)展一個類很危險琳省,父類的具體實(shí)現(xiàn)很容易影響子類的正確性迎吵。而復(fù)合優(yōu)先于繼承告訴我們,不用擴(kuò)展現(xiàn)有的類针贬,而是在新類中增加一個私有域击费,讓它引用現(xiàn)有類的一個實(shí)例。這種設(shè)計稱為復(fù)合(Composition)桦他。
使用復(fù)合和轉(zhuǎn)發(fā)
使用復(fù)合來擴(kuò)展一個類需要實(shí)現(xiàn)兩部分:新的類和可重用的轉(zhuǎn)發(fā)類蔫巩。轉(zhuǎn)發(fā)類用于將所有方法調(diào)用轉(zhuǎn)發(fā)給私有域。這樣得到的類非常穩(wěn)固快压,不依賴于現(xiàn)有類的實(shí)現(xiàn)細(xì)節(jié)圆仔。請看下面的例子。
//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();}
}
現(xiàn)在蔫劣,使用InstrumentedSet不會再出上面的問題了坪郭,因?yàn)闊o論是add
方法還是addAll
方法都轉(zhuǎn)發(fā)給了私有域s
來處理,這些方法對于s
來說總是一致的脉幢,不會受InstrumentedSet的影響截粗。另一個好處是此時的包裝類InstrumentedSet可以用來包裝任何Set實(shí)現(xiàn),有了更廣泛的適用性鸵隧。例如
Set<Date> s = new InstrumentedSet<Date>(new TreeSet<Date>(cmp));
Set<E> s2 = new InstrumentedSet<E>(new HashSet<E>(capacity));
只有當(dāng)子類和超類之間確實(shí)存在父子關(guān)系時绸罗,才可以考慮使用繼承。否則都應(yīng)該用復(fù)合豆瘫,包裝類不僅比子類更加健壯珊蟀,而且功能也更加強(qiáng)大。
關(guān)注作者或文集《Effective Java》,第一時間獲取最新發(fā)布文章育灸。