重新認(rèn)識(shí)java(四) — 組合、聚合與繼承的愛恨情仇

有人學(xué)了繼承,認(rèn)為他是面向?qū)ο筇攸c(diǎn)之一,就在所有能用到繼承的地方使用繼承,而不考慮究竟該不該使用,無疑,這是錯(cuò)誤的。那么,究竟該如何使用繼承呢?

java中類與類之間的關(guān)系

大部分的初學(xué)者只知道java中兩個(gè)類之間可以是繼承與被繼承的關(guān)系,可是事實(shí)上,類之間的關(guān)系大體上存在五種---繼承(實(shí)現(xiàn))、依賴、關(guān)聯(lián)、聚合、組合。

接下來,簡單的分析一下這些關(guān)系。

繼承(實(shí)現(xiàn))

對于類來說,這種關(guān)系叫做繼承,對于接口來說,這種關(guān)系叫做實(shí)現(xiàn)。繼承上一篇文章已經(jīng)詳細(xì)的講解過了,至于實(shí)現(xiàn),我想大家也都知道是怎么回事,由于后面要專門講接口,所以這里就先不說了。繼承是一種“is-a”關(guān)系。

依賴

依賴簡單的理解,就是一個(gè)類A中的方法使用到了另一個(gè)類B。

這種使用關(guān)系是具有偶然性的、臨時(shí)性的、非常弱的,但是B類的變化會(huì)影響到A。

比如說,我用筆寫字,首先需要一個(gè)類來代表我自己,然后需要一個(gè)類來代表一支筆,最后,‘我’要調(diào)用‘筆’里的方法來寫字,用代碼實(shí)現(xiàn)一下:

public class Pen {
    public void write(){
        System.out.println("use pen to write");
    }
}

public class Me {
    public void write(Pen pen){//這里,pen作為Me類方法的參數(shù)
        pen.write();
    }
}

看到這大家都懂了,因?yàn)檫@種代碼你每天都會(huì)寫?,F(xiàn)在你知道了,這就是一種類與類之間的關(guān)系,叫做依賴。

這種關(guān)系是一種很弱的關(guān)系,但是pen類的改變,有可能會(huì)影響到Me類的結(jié)果,比如我把pen類write方法的方法體改了,me中再調(diào)用就會(huì)得到不同的結(jié)果。

一般而言,依賴關(guān)系在Java中體現(xiàn)為局域變量、方法的形參,或者對靜態(tài)方法的調(diào)用。

關(guān)聯(lián)

關(guān)聯(lián)體現(xiàn)的是兩個(gè)類、或者類與接口之間語義級(jí)別的一種強(qiáng)依賴關(guān)系。

這種關(guān)系比依賴更強(qiáng)、不存在依賴關(guān)系的偶然性、關(guān)系也不是臨時(shí)性的,一般是長期性的,而且雙方的關(guān)系一般是平等的、關(guān)聯(lián)可以是單向、雙向的。

看下面這段代碼:

// pen 還是上面的pen
public class You {
    private Pen pen; // 讓pen成為you的類屬性 
    
    public You(Pen p){
        this.pen = p;
    }
    
    public void write(){
        pen.write();
    }
}

被關(guān)聯(lián)類B以類屬性的形式出現(xiàn)在關(guān)聯(lián)類A中,或者關(guān)聯(lián)類A引用了一個(gè)類型為被關(guān)聯(lián)類B的全局變量的這種關(guān)系,就叫關(guān)聯(lián)關(guān)系。

在Java中,關(guān)聯(lián)關(guān)系一般使用成員變量來實(shí)現(xiàn)。

聚合

聚合是關(guān)聯(lián)關(guān)系的一種特例,他體現(xiàn)的是整體與部分、擁有的關(guān)系,即has-a的關(guān)系

看下面一段代碼:

public class Family {
    private List<Child> children; //一個(gè)家庭里有許多孩子
    
    // ...
}

在代碼層面,聚合和關(guān)聯(lián)關(guān)系是一致的,只能從語義級(jí)別來區(qū)分。普通的關(guān)聯(lián)關(guān)系中,a類和b類沒有必然的聯(lián)系,而聚合中,需要b類是a類的一部分,是一種”has-a“的關(guān)系,即 a has-a b; 比如家庭有孩子,屋子里有空調(diào)。

但是,has 不是 must has,a可以有b,也可以沒有。a是整體,b是部分,整體與部分之間是可分離的,他們可以具有各自的生命周期,部分可以屬于多個(gè)整體對象,也可以為多個(gè)整體對象共享。

不同于關(guān)聯(lián)關(guān)系的平等地位,聚合關(guān)系中兩個(gè)類的地位是不平等。

組合

組合也是關(guān)聯(lián)關(guān)系的一種特例,他體現(xiàn)的是一種contains-a的關(guān)系,這種關(guān)系比聚合更強(qiáng),也稱為強(qiáng)聚合。

先看一段代碼:

public class Nose {
    private Eye eye = new Eye();  //一個(gè)人有鼻子有眼睛
    private Nose nose = new Nose();
    
    // .... 
}

組合同樣體現(xiàn)整體與部分間的關(guān)系,但此時(shí)整體與部分是不可分的,整體的生命周期結(jié)束也就意味著部分的生命周期結(jié)束。

就像你有鼻子有眼睛,如果你一不小心結(jié)束了生命周期,鼻子和眼睛的生命周期也會(huì)結(jié)束,而且,鼻子和眼睛不能脫離你單獨(dú)存在。

只看代碼,你是無法區(qū)分關(guān)聯(lián),聚合和組合的,具體是哪一種關(guān)系,只能從語義級(jí)別來區(qū)分。

同樣,組合關(guān)系中,兩個(gè)類額關(guān)系也是不平等的。

組合,聚合和繼承

依賴關(guān)系是每一個(gè)java程序都離不開的,所以就不單獨(dú)討論了,普通的關(guān)聯(lián)關(guān)系也沒有什么特殊的地方,下面我們重點(diǎn)研究一下組合,聚合和繼承。

聚合與組合

  1. 聚合與組合都是一種關(guān)聯(lián)關(guān)系,只是額外具有整體-部分的意義。

  2. 部件的生命周期不同

    • 聚合關(guān)系中,整件不會(huì)擁有部件的生命周期,所以整件刪除時(shí),部件不會(huì)被刪除。再者,多個(gè)整件可以共享同一個(gè)部件。

    • 組合關(guān)系中,整件擁有部件的生命周期,所以整件刪除時(shí),部件一定會(huì)跟著刪除。而且,多個(gè)整件不可以同時(shí)間共享同一個(gè)部件。

    這個(gè)區(qū)別可以用來區(qū)分某個(gè)關(guān)聯(lián)關(guān)系到底是組合還是聚合。兩個(gè)類生命周期不同步,則是聚合關(guān)系,生命周期同步就是組合關(guān)系。

  3. 聚合關(guān)系是【has-a】關(guān)系,組合關(guān)系是【contains-a】關(guān)系。

    平時(shí)我們只討論組合和繼承的時(shí)候,認(rèn)為組合是【has-a 】關(guān)系,而事實(shí)上,聚合才是真正的【has-a】關(guān)系,組合是更深層次的【contains-a】關(guān)系。

由于【contains-a】關(guān)系是一種更深的【has-a】關(guān)系,所以說組合是【has-a】關(guān)系也是正確的。

組合和繼承

這個(gè)才是本文的重點(diǎn)。

學(xué)過設(shè)計(jì)模式的都知道,要“少用繼承,多用組合”,這究竟是為什么呢?

我們先來看一下組合和繼承各自的優(yōu)缺點(diǎn):

組合和繼承的優(yōu)缺點(diǎn)

組合

優(yōu)點(diǎn):

- 不破壞封裝,整體類與局部類之間松耦合,彼此相對獨(dú)立
- 具有較好的可擴(kuò)展性
- 支持動(dòng)態(tài)組合。在運(yùn)行時(shí),整體對象可以選擇不同類型的局部對象
- 整體類可以對局部類進(jìn)行包裝,封裝局部類的接口,提供新的接口

缺點(diǎn):

- 整體類不能自動(dòng)獲得和局部類同樣的接口
- 創(chuàng)建整體類的對象時(shí),需要?jiǎng)?chuàng)建所有局部類的對象

缺點(diǎn)分析:

1、整體類不能自動(dòng)獲得和局部類同樣的接口

如果父類的方法子類中幾乎都要暴露出去,這時(shí)可能會(huì)覺得使用組合很不方便,使用繼承似乎更簡單方便。但從另一個(gè)角度講,實(shí)際上也許子類中并不需要暴露這些方法,客戶端組合應(yīng)用就可以了。所以上邊推薦不要繼承那些不是為了繼承而設(shè)計(jì)的類,一般為了繼承而設(shè)計(jì)的類都是抽象類。

2、創(chuàng)建整體類的對象時(shí),需要?jiǎng)?chuàng)建所有局部類的對象

這個(gè)可能沒什么更好的辦法,但在實(shí)際應(yīng)用中并沒有多出多少代碼。

繼承

優(yōu)點(diǎn):

- 子類能自動(dòng)繼承父類的接口
- 創(chuàng)建子類的對象時(shí),無須創(chuàng)建父類的對象

缺點(diǎn):

- 破壞封裝,子類與父類之間緊密耦合,子類依賴于父類的實(shí)現(xiàn),子類缺乏獨(dú)立性
- 支持?jǐn)U展,但是往往以增加系統(tǒng)結(jié)構(gòu)的復(fù)雜度為代價(jià)
- 不支持動(dòng)態(tài)繼承。在運(yùn)行時(shí),子類無法選擇不同的父類
- 子類不能改變父類的接口

缺點(diǎn)分析:

1、為什么繼承破壞封裝性?

>
這里寫圖片描述
這里寫圖片描述

鴨子中不想要“飛”的方法,但因?yàn)槔^承無法封裝這個(gè)無用的“飛”方法 。

2、為什么繼承緊耦合:

這里寫圖片描述
這里寫圖片描述

當(dāng)作為父類的BaseTable中感覺Insert這個(gè)名字不合適時(shí),如果希望將其修改成Create方法,那使用了子類對象Insert方法將會(huì)編譯出錯(cuò),可能你會(huì)覺得這改起來還算容易,因?yàn)橛兄貥?gòu)工具一下子就好了并且編譯錯(cuò)誤改起來很容易。但如果BaseTable和子類在不同的程序集中,維護(hù)的人員不同,BaseTable程序集升級(jí),那本來能用的代碼忽然不能用了,這還是很難讓人接受的

3、為什么繼承擴(kuò)展起來比較復(fù)雜

這里寫圖片描述
這里寫圖片描述

當(dāng)圖書和數(shù)碼的算稅方式和數(shù)碼產(chǎn)品一樣時(shí),而消費(fèi)類產(chǎn)品的算稅方式是另一樣時(shí),如果采用繼承方案可能會(huì)演變成如下方式:

>

這樣如果產(chǎn)品繼續(xù)增加,算稅方式繼續(xù)增加,那繼承的層次會(huì)非常復(fù)雜,而且很難控制,而使用組合就能很好的解決這個(gè)問題

4、繼承不能支持動(dòng)態(tài)繼承

這個(gè)其實(shí)很好理解,因?yàn)槔^承是編譯期就決定下來的,無法在運(yùn)行時(shí)改變,如3例中,如果用戶需要根據(jù)當(dāng)?shù)氐那闆r選擇計(jì)稅方式,使用繼承就解決不了,而使用組合結(jié)合反射就能很好的解決。

5、為什么繼承,子類不能改變父類接口

如2中的圖,子類中覺得Insert方法不合適,希望使用Create方法,因?yàn)槔^承的原因無法改變

組合與繼承的區(qū)別和聯(lián)系

  • 在繼承結(jié)構(gòu)中,父類的內(nèi)部細(xì)節(jié)對于子類是可見的。所以我們通常也可以說通過繼承的代碼復(fù)用是一種 白盒式代碼復(fù)用。(如果基類的實(shí)現(xiàn)發(fā)生改變,那么派生類的實(shí)現(xiàn)也將隨之改變。這樣就導(dǎo)致了子類行為的不可預(yù)知性)

  • 組合是通過對現(xiàn)有的對象進(jìn)行拼裝(組合)產(chǎn)生新的、更復(fù)雜的功能。因?yàn)樵趯ο笾g,各自的內(nèi)部細(xì)節(jié)是不可見的,所以我們也說這種方式的代碼復(fù)用是黑盒式代碼復(fù)用 。(因?yàn)榻M合中一般都定義一個(gè)類型,所以在編譯期根本不知道具體會(huì)調(diào)用哪個(gè)實(shí)現(xiàn)類的方法)

  • 繼承在寫代碼的時(shí)候就要指名具體繼承哪個(gè)類,所以,在編譯期就確定了關(guān)系。(從基類繼承來的實(shí)現(xiàn)是無法在運(yùn)行期動(dòng)態(tài)改變的,因此降低了應(yīng)用的靈活性。)

  • 組合,在寫代碼的時(shí)候可以采用面向接口編程。所以,類的組合關(guān)系一般在運(yùn)行期確定。

  • 組合(has-a)關(guān)系可以顯式地獲得被包含類(繼承中稱為父類)的對象,而繼承(is-a)則是隱式地獲得父類的對象,被包含類和父類對應(yīng),而組合外部類和子類對應(yīng)。

  • 組合是在組合類和被包含類之間的一種松耦合關(guān)系,而繼承則是父類和子類之間的一種緊耦合關(guān)系。

  • 當(dāng)選擇使用組合關(guān)系時(shí),在組合類中包含了外部類的對象,組合類可以調(diào)用外部類必須的方法,而使用繼承關(guān)系時(shí),父類的所有方法和變量都被子類無條件繼承,子類不能選擇。

  • 最重要的一點(diǎn),使用繼承關(guān)系時(shí),可以實(shí)現(xiàn)類型的回溯,即用父類變量引用子類對象,這樣便可以實(shí)現(xiàn)多態(tài),而組合沒有這個(gè)特性。

  • 還有一點(diǎn)需要注意,如果你確定復(fù)用另外一個(gè)類的方法永遠(yuǎn)不需要改變時(shí),應(yīng)該使用組合,因?yàn)榻M合只是簡單地復(fù)用被包含類的接口,而繼承除了復(fù)用父類的接口外,它甚至還可以覆蓋這些接口,修改父類接口的默認(rèn)實(shí)現(xiàn),這個(gè)特性是組合所不具有的。

  • 從邏輯上看,組合最主要地體現(xiàn)的是一種整體和部分的思想,例如在電腦類是由內(nèi)存類,CPU類,硬盤類等等組成的,而繼承則體現(xiàn)的是一種可以回溯的父子關(guān)系,子類也是父類的一個(gè)對象。

  • 這兩者的區(qū)別主要體現(xiàn)在類的抽象階段,在分析類之間的關(guān)系時(shí)就應(yīng)該確定是采用組合還是采用繼承。

  • 引用網(wǎng)友的一句很經(jīng)典的話應(yīng)該更能讓大家分清繼承和組合的區(qū)別:組合可以被說成“我請了個(gè)老頭在我家里干活” ,繼承則是“我父親在家里幫我干活"。

**繼承還是組合? **

首先它們都是實(shí)現(xiàn)系統(tǒng)功能重用,代碼復(fù)用的最常用的有效的設(shè)計(jì)技巧,都是在設(shè)計(jì)模式中的基礎(chǔ)結(jié)構(gòu)。

很多人都知道面向?qū)ο笾杏幸粋€(gè)比較重要的原則『多用組合、少用繼承』或者說『組合優(yōu)于繼承』。從前面的介紹已經(jīng)優(yōu)缺點(diǎn)對比中也可以看出,組合確實(shí)比繼承更加靈活,也更有助于代碼維護(hù)。

所以,建議在同樣可行的情況下,優(yōu)先使用組合而不是繼承。因?yàn)榻M合更安全,更簡單,更靈活,更高效。

注意,并不是說繼承就一點(diǎn)用都沒有了,前面說的是【在同樣可行的情況下】。有一些場景還是需要使用繼承的,或者是更適合使用繼承。

繼承要慎用,其使用場合僅限于你確信使用該技術(shù)有效的情況。一個(gè)判斷方法是,問一問自己是否需要從新類向基類進(jìn)行向上轉(zhuǎn)型。如果是必須的,則繼承是必要的。反之則應(yīng)該好好考慮是否需要繼承。

只有當(dāng)子類真正是超類的子類型時(shí),才適合用繼承。換句話說,對于兩個(gè)類A和B,只有當(dāng)兩者之間確實(shí)存在 is-a 關(guān)系的時(shí)候,類B才應(yīng)該繼承類A。

向上轉(zhuǎn)型將會(huì)在下一篇《重新認(rèn)識(shí)Java(五) --- 面向?qū)ο笾鄳B(tài)》中詳細(xì)講解。

總結(jié)

根據(jù)我們前面講的內(nèi)容我們可以發(fā)現(xiàn)繼承的缺點(diǎn)遠(yuǎn)遠(yuǎn)多于優(yōu)點(diǎn),盡管繼承在學(xué)習(xí)OOP的過程中得到了大量的強(qiáng)調(diào),但并不意味著應(yīng)該盡可能地到處使用它。相反,使用它時(shí)要特別慎重。

只有在清楚知道繼承在所有方法中最有效的前提下,才可考慮它。 繼承最大的優(yōu)點(diǎn)就是擴(kuò)展簡單,但大多數(shù)缺點(diǎn)都很致命,但是因?yàn)檫@個(gè)擴(kuò)展簡單的優(yōu)點(diǎn)太明顯了,很多人并不深入思考,所以造成了太多問題。

最后,總結(jié)一下:

1、精心設(shè)計(jì)專門用于被繼承的類,繼承樹的抽象層應(yīng)該比較穩(wěn)定,一般不要多于三層。
2、對于不是專門用于被繼承的類,禁止其被繼承。
3、優(yōu)先考慮用組合關(guān)系來提高代碼的可重用性。
4、子類是一種特殊的類型,而不只是父類的一個(gè)角色
5、子類擴(kuò)展,而不是覆蓋或者使父類的功能失效

沒錯(cuò),寫了這么多,就是想說: 請慎重使用繼承,除非你確定非用繼承不可!


這篇文章寫得比較粗糙,因?yàn)閷懳恼碌臅r(shí)候一直在拉肚子。。。以后還會(huì)做一些修改,暫時(shí)先這樣。如果文中有錯(cuò)誤或者有更好的解釋,歡迎給我留言。我也只是一個(gè)學(xué)習(xí)的人,而不是一個(gè)Java大神,所以不保證文章內(nèi)容的正確性~

參考文章:
http://www.cnblogs.com/nuaalfm/archive/2010/04/23/1718453.html
http://xifangyuhui.iteye.com/blog/819498
http://www.tuicool.com/articles/u2uUZjb
http://www.cnblogs.com/jiqing9006/p/5915023.html

本文地址
http://www.itdecent.cn/p/edca9422b203
轉(zhuǎn)載請注明出處。
看完點(diǎn)個(gè)贊唄。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,627評論 18 399
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,506評論 19 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,802評論 25 709
  • 你可以沒有理想,但不可以不學(xué)習(xí); 你可以學(xué)得不好,但不可以不進(jìn)步; 你可以不進(jìn)步,但不可以無所事事; 你可以不做正...
    如一書閱讀 262評論 1 1
  • 展會(huì)機(jī)緣聚鵬城, 異地相逢情更長。 軟塑事業(yè)大平臺(tái), 各奮前程泛容光。 萬壽歲月雖匆匆, 中達(dá)情誼深似潭。 愿將今...
    潤德齋主人閱讀 254評論 0 0

友情鏈接更多精彩內(nèi)容