Effective-java 3 中文翻譯系列 (Item 18 重組合嗦哆,輕繼承)

原文鏈接

文章也上傳到

github

(歡迎關(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)大穿肄。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末年局,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子咸产,更是在濱河造成了極大的恐慌矢否,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件脑溢,死亡現(xiàn)場離奇詭異僵朗,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)屑彻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門验庙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人社牲,你說我怎么就攤上這事粪薛。” “怎么了搏恤?”我有些...
    開封第一講書人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵违寿,是天一觀的道長。 經(jīng)常有香客問我熟空,道長藤巢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任痛阻,我火速辦了婚禮菌瘪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘阱当。我一直安慰自己俏扩,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開白布弊添。 她就那樣靜靜地躺著录淡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪油坝。 梳的紋絲不亂的頭發(fā)上嫉戚,一...
    開封第一講書人閱讀 51,462評(píng)論 1 302
  • 那天刨裆,我揣著相機(jī)與錄音,去河邊找鬼彬檀。 笑死帆啃,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的窍帝。 我是一名探鬼主播努潘,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼坤学!你這毒婦竟也來了疯坤?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤深浮,失蹤者是張志新(化名)和其女友劉穎压怠,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體飞苇,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡菌瘫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了玄柠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片突梦。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖羽利,靈堂內(nèi)的尸體忽然破棺而出宫患,到底是詐尸還是另有隱情,我是刑警寧澤这弧,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布娃闲,位于F島的核電站,受9級(jí)特大地震影響匾浪,放射性物質(zhì)發(fā)生泄漏皇帮。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一蛋辈、第九天 我趴在偏房一處隱蔽的房頂上張望属拾。 院中可真熱鬧,春花似錦冷溶、人聲如沸渐白。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽纯衍。三九已至,卻和暖如春苗胀,著一層夾襖步出監(jiān)牢的瞬間襟诸,已是汗流浹背瓦堵。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留歌亲,地道東北人菇用。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像应结,于是被迫代替她去往敵國和親刨疼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • (一)Java部分 1鹅龄、列舉出JAVA中6個(gè)比較常用的包【天威誠信面試題】 【參考答案】 java.lang;ja...
    獨(dú)云閱讀 7,104評(píng)論 0 62
  • 我的老家在東北吉林省九臺(tái)區(qū)。在那邊用我爸的話說叫:“像蹲監(jiān)獄一樣亭畜,沒有網(wǎng)絡(luò)扮休,沒有信號(hào),電話也打不出去拴鸵,憋了一天才只...
    佛語空靈閱讀 2,306評(píng)論 0 1
  • 什么是理財(cái)? 理財(cái)是一場長跑馬拉松戰(zhàn)役聘芜,他不是富人專有的權(quán)利兄渺,投資要趕早,只要有開源節(jié)流的意識(shí)汰现,并持之以恒挂谍,就會(huì)有...
    琉森521閱讀 634評(píng)論 11 11
  • 《清風(fēng)引》 文|蘇吉兒 清風(fēng)切 紅梅落地迎桃曳, 迎桃曳瞎饲。 年年輪轉(zhuǎn)口叙,花開花謝。 古今幾度傷離別嗅战, 夢(mèng)中又見家...
    敏慧琳閱讀 221評(píng)論 0 1
  • 任何一件事開始的時(shí)候都要經(jīng)歷一段適應(yīng)期妄田,這段適應(yīng)期往往很艱苦,效率很低驮捍。而如果總是中斷計(jì)劃疟呐,就會(huì)產(chǎn)生無數(shù)的適應(yīng)期,...
    洛尓閱讀 353評(píng)論 0 0