原文鏈接
文章也上傳到
(歡迎關(guān)注谤祖,歡迎大神提點(diǎn)。)
ITEM 18 重組合老速,輕繼承
繼承是實(shí)現(xiàn)代碼重用的有效方法粥喜,但是使用的不恰當(dāng)?shù)脑捑蜁?huì)導(dǎo)致軟件變得脆弱。在一個(gè)包里使用繼承是安全的橘券,這里子類和父類都由同一個(gè)程序員控制额湘。在特定的設(shè)計(jì)和文檔說明下使用也是安全的(Item19)。但是跨越包繼承就是危險(xiǎn)的了旁舰。這里說的繼承指的是實(shí)現(xiàn)繼承(一個(gè)類extends另外一個(gè)類)锋华,不包括接口的繼承(一個(gè)類實(shí)現(xiàn)一個(gè)接口或一個(gè)接口繼承另一個(gè)接口)。
繼承破壞了封裝性箭窜。換句話說毯焕,子類過于依賴父類的實(shí)現(xiàn)細(xì)節(jié)。然而父類的實(shí)現(xiàn)可能隨著版本更替而發(fā)生改變绽快,這時(shí)候子類就算沒有任何改動(dòng)也可能會(huì)崩潰芥丧。結(jié)果就是:子類必須隨著父類的變化而變化,除非父類的作者有文檔說明或者這個(gè)類本身就是用來繼承的坊罢。
舉個(gè)例子续担,假如我們有個(gè)程序使用HashSet,我們需要計(jì)算一共添加到這個(gè)HashSet多少個(gè)元素(當(dāng)刪除元素時(shí)活孩,會(huì)減少標(biāo)志)物遇。為了實(shí)現(xiàn)這個(gè)需求,我們給HashSet添加一個(gè)計(jì)數(shù)變量并提供一個(gè)getter方法返回這個(gè)變量的值憾儒。已知的是HashSet類有兩個(gè)方法可以添加元素询兴,add和addAll。所以我們重寫這些方法:
//不恰當(dāng)?shù)氖褂美^承例子
public class InstrumentedHashSet<E> extends HashSet<E> {
// The number of attempted element insertions
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;
}
}
這個(gè)例子看著沒啥問題起趾,但是卻是不能正常工作的诗舰。假設(shè)我們創(chuàng)建一個(gè)實(shí)例然后使用addAll給它添加3個(gè)元素。(順便提一句训裆,Java9中我們創(chuàng)建一個(gè)list可使用靜態(tài)工廠方法List.of眶根,之前我們使用的是Arrays.asList):
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("Snap", "Crackle", "Pop"));
然后我們調(diào)用getAddCount方法獲取結(jié)果,發(fā)現(xiàn)結(jié)果不是3而是6边琉。難道我們哪里寫錯(cuò)了么属百?
原來在HashSet內(nèi)部,addAll方法會(huì)調(diào)用自己的add方法变姨。所以每個(gè)元素被加了兩次族扰。
我們可以修復(fù)這個(gè)問題通過移除addAll方法,雖然這么做可以修復(fù)這個(gè)問題,但是這樣的做法的前提是建立在HashSet的addAll方法調(diào)用了內(nèi)部add方法的基礎(chǔ)上渔呵。而這個(gè)前提并不能保證隨著Java版本的更替不會(huì)被改變怒竿。因此,InstrumentedHashSet這個(gè)類就變的很脆弱了厘肮。
有一種稍微更好點(diǎn)的做法是重寫addAll方法的實(shí)現(xiàn)愧口,使用迭代器把每一個(gè)元素使用add方法添加到指定的collection中,這樣的話不管原來addAll方法有沒有調(diào)用add都沒關(guān)系了类茂,因?yàn)楝F(xiàn)在不會(huì)調(diào)用父類的addAll的實(shí)現(xiàn)了耍属。然而,這樣還是沒有解決我們所有的問題巩检。因?yàn)檫@時(shí)候當(dāng)父類中如果存在調(diào)用自己的addAll將會(huì)出問題厚骗,而且可能會(huì)導(dǎo)致方法更耗時(shí),降低性能等等兢哭。另外领舰,這些方法有時(shí)候會(huì)被設(shè)置成private的,也不總是會(huì)允許子類重寫迟螺。如果父類在之后的版本中增加新的方法來添加元素也將導(dǎo)致子類出問題冲秽,因?yàn)榇藭r(shí)子類沒有在新方法中實(shí)現(xiàn)自己的計(jì)數(shù)功能。當(dāng)Hashtable和Vector被升級(jí)進(jìn)入Collections Framework時(shí)類似的安全漏洞必須被修復(fù)矩父。
這些問題都源于overriding 方法锉桑。你可能會(huì)想繼承一個(gè)類僅僅添加新的方法而不重寫就是安全的。確實(shí)這樣會(huì)更加安全一點(diǎn)窍株,但是也不是沒有一點(diǎn)風(fēng)險(xiǎn)民轴,如果父類在之后的版本中添加了一個(gè)和你同名但返回值不同的方法,那么你的類將編譯不通過球订。如果父類添加了一個(gè)和你同名返回值也相同的方法后裸,那就相當(dāng)于你重寫了這個(gè)方法,上面說的問題就又出現(xiàn)了冒滩。
幸運(yùn)的是微驶,有一種方法可以避免上面提到的所有問題。不是繼承于一個(gè)現(xiàn)有的類开睡,而是添加一個(gè)對(duì)現(xiàn)有類的私有屬性祈搜,這種設(shè)計(jì)叫做組合,因?yàn)橐汛嬖诘念惓闪诵骂惖慕M件士八。新類中的這個(gè)實(shí)例調(diào)用現(xiàn)有類相應(yīng)的方法并返回相應(yīng)結(jié)果,這叫轉(zhuǎn)發(fā)梁呈。因?yàn)樾骂惒灰蕾囉诂F(xiàn)有類的實(shí)現(xiàn)細(xì)節(jié)婚度,所以會(huì)變得相對(duì)健壯,不會(huì)跟現(xiàn)有類的方法產(chǎn)生沖突。這里提供一種替代InstrumentedHashSet類的實(shí)現(xiàn)方法蝗茁,使用組件和轉(zhuǎn)發(fā)方法醋虏。注意:這里的實(shí)現(xiàn)分成兩個(gè)類,類本身和一個(gè)可重用的轉(zhuǎn)發(fā)類哮翘,它包含所有要轉(zhuǎn)發(fā)的方法:
//包裝類
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;
}
}
//可重用的轉(zhuǎn)發(fā)類
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) { this.s = s; }
public void clear() { s.clear(); }
public boolean contains(Object o) { return s.contains(o); }
public boolean isEmpty() { return s.isEmpty(); }
public int size() { return s.size(); }
public Iterator<E> iterator() { return s.iterator(); }
public boolean add(E e) { return s.add(e); }
public boolean remove(Object o) { return s.remove(o); }
public boolean containsAll(Collection<?> c)
{ return s.containsAll(c); }
public boolean addAll(Collection<? extends E> c)
{ return s.addAll(c); }
public boolean removeAll(Collection<?> c)
{ return s.removeAll(c); }
public boolean retainAll(Collection<?> c)
{ return s.retainAll(c); }
public Object[] toArray() { return s.toArray(); }
public <T> T[] toArray(T[] a) { return s.toArray(a); }
@Override public boolean equals(Object o)
{ return s.equals(o); }
@Override public int hashCode() { return s.hashCode(); }
@Override public String toString() { return s.toString(); }
}
InstrumentedSet類基于Set接口實(shí)現(xiàn)颈嚼,該接口包含了HashSet的功能特性。InstrumentedSet類實(shí)現(xiàn)了Set接口并提供一個(gè)接受Set類型的構(gòu)造方法饭寺。本質(zhì)上講阻课,這種實(shí)現(xiàn)是將一個(gè)Set轉(zhuǎn)換成了另一個(gè),并添加了自己的計(jì)數(shù)功能艰匙。不像繼承那樣只能提供單一功能而且要求實(shí)現(xiàn)父類的一系列構(gòu)造方法限煞,這個(gè)包裝類可以實(shí)現(xiàn)任何set工具,并可以結(jié)合現(xiàn)有的任何構(gòu)造方法一起使用员凝。
Set<Instant> times = new InstrumentedSet<>(new TreeSet<>(cmp));
Set<E> s = new InstrumentedSet<>(new HashSet<>(INIT_CAPACITY));
InstrumentedSet類甚至可以臨時(shí)被用于替換沒有相應(yīng)功能的set實(shí)例:
static void walk(Set<Dog> dogs) {
InstrumentedSet<Dog> iDogs = new InstrumentedSet<>(dogs);
... // Within this method use iDogs instead of dogs
}
因?yàn)槊恳粋€(gè)InstrumentedSet實(shí)例都包含另一個(gè)set對(duì)象署驻,所以被稱為包裝類。這也被稱為裝飾器模式健霹,如InstrumentedSet類用計(jì)數(shù)方法裝飾類set類旺上。有時(shí)候來看,組合和轉(zhuǎn)發(fā)的結(jié)合體非常類似于代理模式糖埋。但是只要包裝對(duì)象沒有把自己傳遞給別的包裝對(duì)象就不算是代理宣吱。
包裝類的缺點(diǎn)很少,但有一點(diǎn)是包裝類不能被用在回調(diào)型的框架中阶捆。在包裝類中凌节,回調(diào)時(shí)調(diào)用的是對(duì)象本身,因?yàn)閷?duì)象發(fā)送對(duì)自己的引用給其他的對(duì)象洒试。這被叫做SELF問題倍奢。因?yàn)楸话b的類不知道誰是它的包裝類,所以回調(diào)也就避開了包裝器垒棋。有人擔(dān)心轉(zhuǎn)發(fā)類的性能和內(nèi)存占用問題卒煞,但是實(shí)際證明這兩者都不會(huì)造成太大的問題。寫轉(zhuǎn)發(fā)類是乏味的叼架,而且你不得不為每一個(gè)接口寫一次轉(zhuǎn)發(fā)類畔裕。
只有當(dāng)一個(gè)類的類型真正屬于另一個(gè)類的時(shí)候才適合使用繼承。換句話說乖订,就是類B繼承于類A時(shí)扮饶,一定是當(dāng)“is-A”的時(shí)候。如果當(dāng)你的類B要繼承于類A的時(shí)候乍构,要問自己:B真的屬于A類型么甜无?如果你不能確定,那就不應(yīng)該使用繼承。如果回答是No岂丘,那么B應(yīng)該包含一個(gè)類A的私有對(duì)象陵究。在Java平臺(tái)很多地方都違背了這個(gè)原則,例如Stack類不屬于Vector奥帘,但是繼承于Vector铜邮;同樣一個(gè)屬性list不是一個(gè)hash table,但是Perperties繼承于HashTable寨蹋。在這兩種情況下松蒜,組合都會(huì)更好點(diǎn)。
如果在適合使用組合的地方使用了繼承钥庇,就會(huì)暴漏實(shí)現(xiàn)細(xì)節(jié)牍鞠。會(huì)被原始的實(shí)現(xiàn)限制住,永遠(yuǎn)限制了你類的性能评姨。更嚴(yán)重的是难述,暴漏內(nèi)部實(shí)現(xiàn)是你的客戶端(使用者)直接訪問到內(nèi)部。至少吐句,它會(huì)導(dǎo)致語意混亂胁后。例如,如果p引用一個(gè)Properties對(duì)象嗦枢,然后p.getProperty(key)可能會(huì)和p.get(key)產(chǎn)生混亂攀芯,因?yàn)楹笳呤菑腍ashtable繼承而來的方法。更嚴(yán)重的是文虏,子類可能通過直接修改父類會(huì)破環(huán)父類的不變性侣诺。至于Properties,設(shè)計(jì)者希望只有String 類型被允許當(dāng)作keys和values氧秘,但是由于繼承于Hashtable年鸳,父類的put和putAll允許其他類型。一旦在遭到破壞的Properties對(duì)象上使用load和store將會(huì)不成功丸相。當(dāng)發(fā)現(xiàn)這個(gè)問題時(shí)也為時(shí)以晚搔确,因?yàn)橐呀?jīng)有很多客戶端已經(jīng)基于這個(gè)類使用了非string類型的key和value。如java庫中的描述:
* Because <code>Properties</code> inherits from <code>Hashtable</code>, the
* <code>put</code> and <code>putAll</code> methods can be applied to a
* <code>Properties</code> object. Their use is strongly discouraged as they
* allow the caller to insert entries whose keys or values are not
* <code>Strings</code>. The <code>setProperty</code> method should be used
* instead. If the <code>store</code> or <code>save</code> method is called
* on a "compromised" <code>Properties</code> object that contains a
* non-<code>String</code> key or value, the call will fail. Similarly,
* the call to the <code>propertyNames</code> or <code>list</code> method
* will fail if it is called on a "compromised" <code>Properties</code>
* object that contains a non-<code>String</code> key.
當(dāng)你要使用繼承代替組合時(shí)灭忠,問自己最后一個(gè)問題:繼承的父類的api有沒有瑕疵呢膳算?如果有你是否愿意將這種瑕疵繼承到自己的api中?繼承會(huì)將父類的瑕疵傳遞下來弛作,而組合可以允許你設(shè)計(jì)新的api隱藏這些瑕疵涕蜂。
總結(jié)起來,繼承是強(qiáng)大的映琳,但是它會(huì)造成很多問題宇葱,因?yàn)樗鼤?huì)破壞封裝性瘦真。它僅適合當(dāng)子類和父類是is-a的關(guān)系時(shí)使用。不僅如此黍瞧,繼承還可能導(dǎo)致類的脆弱,假如類繼承于不同包的類或父類不是被用于繼承設(shè)計(jì)時(shí)原杂。為了避免這種問題印颤,使用組合和轉(zhuǎn)發(fā)來代替繼承。使用包裝類不僅比繼承更加健壯而且也更加強(qiáng)大穿肄。