有人學(xué)了繼承请敦,認(rèn)為他是面向?qū)ο筇攸c(diǎn)之一镐躲,就在所有能用到繼承的地方使用繼承,而不考慮究竟該不該使用侍筛,無(wú)疑萤皂,這是錯(cuò)誤的。那么匣椰,究竟該如何使用繼承呢裆熙?
java中類(lèi)與類(lèi)之間的關(guān)系
大部分的初學(xué)者只知道java中兩個(gè)類(lèi)之間可以是繼承與被繼承的關(guān)系,可是事實(shí)上,類(lèi)之間的關(guān)系大體上存在五種---繼承(實(shí)現(xiàn))入录、依賴(lài)蛤奥、關(guān)聯(lián)、聚合僚稿、組合凡桥。
接下來(lái),簡(jiǎn)單的分析一下這些關(guān)系贫奠。
繼承(實(shí)現(xiàn))
對(duì)于類(lèi)來(lái)說(shuō)唬血,這種關(guān)系叫做繼承,對(duì)于接口來(lái)說(shuō)唤崭,這種關(guān)系叫做實(shí)現(xiàn)拷恨。繼承上一篇文章已經(jīng)詳細(xì)的講解過(guò)了,至于實(shí)現(xiàn)谢肾,我想大家也都知道是怎么回事腕侄,由于后面要專(zhuān)門(mén)講接口,所以這里就先不說(shuō)了芦疏。繼承是一種“is-a”關(guān)系冕杠。
依賴(lài)
依賴(lài)簡(jiǎn)單的理解,就是一個(gè)類(lèi)A中的方法使用到了另一個(gè)類(lèi)B酸茴。
這種使用關(guān)系是具有偶然性的分预、臨時(shí)性的、非常弱的薪捍,但是B類(lèi)的變化會(huì)影響到A笼痹。
比如說(shuō),我用筆寫(xiě)字酪穿,首先需要一個(gè)類(lèi)來(lái)代表我自己凳干,然后需要一個(gè)類(lèi)來(lái)代表一支筆,最后被济,‘我’要調(diào)用‘筆’里的方法來(lái)寫(xiě)字救赐,用代碼實(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類(lèi)方法的參數(shù)
pen.write();
}
}
看到這大家都懂了只磷,因?yàn)檫@種代碼你每天都會(huì)寫(xiě)【酰現(xiàn)在你知道了,這就是一種類(lèi)與類(lèi)之間的關(guān)系钮追,叫做依賴(lài)预厌。
這種關(guān)系是一種很弱的關(guān)系,但是pen類(lèi)的改變畏陕,有可能會(huì)影響到Me類(lèi)的結(jié)果配乓,比如我把pen類(lèi)write方法的方法體改了,me中再調(diào)用就會(huì)得到不同的結(jié)果。
一般而言犹芹,依賴(lài)關(guān)系在Java中體現(xiàn)為局域變量崎页、方法的形參,或者對(duì)靜態(tài)方法的調(diào)用腰埂。
關(guān)聯(lián)
關(guān)聯(lián)體現(xiàn)的是兩個(gè)類(lèi)飒焦、或者類(lèi)與接口之間語(yǔ)義級(jí)別的一種強(qiáng)依賴(lài)關(guān)系。
這種關(guān)系比依賴(lài)更強(qiáng)屿笼、不存在依賴(lài)關(guān)系的偶然性牺荠、關(guān)系也不是臨時(shí)性的,一般是長(zhǎng)期性的驴一,而且雙方的關(guān)系一般是平等的休雌、關(guān)聯(lián)可以是單向、雙向的肝断。
看下面這段代碼:
// pen 還是上面的pen
public class You {
private Pen pen; // 讓pen成為you的類(lèi)屬性
public You(Pen p){
this.pen = p;
}
public void write(){
pen.write();
}
}
被關(guān)聯(lián)類(lèi)B以類(lèi)屬性的形式出現(xiàn)在關(guān)聯(lián)類(lèi)A中杈曲,或者關(guān)聯(lián)類(lèi)A引用了一個(gè)類(lèi)型為被關(guān)聯(lián)類(lèi)B的全局變量的這種關(guān)系,就叫關(guān)聯(lián)關(guān)系胸懈。
在Java中担扑,關(guān)聯(lián)關(guān)系一般使用成員變量來(lái)實(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)系是一致的燕垃,只能從語(yǔ)義級(jí)別來(lái)區(qū)分。普通的關(guān)聯(lián)關(guān)系中绞灼,a類(lèi)和b類(lèi)沒(méi)有必然的聯(lián)系利术,而聚合中呈野,需要b類(lèi)是a類(lèi)的一部分低矮,是一種”has-a“的關(guān)系,即 a has-a b; 比如家庭有孩子被冒,屋子里有空調(diào)军掂。
但是,has 不是 must has昨悼,a可以有b蝗锥,也可以沒(méi)有。a是整體率触,b是部分终议,整體與部分之間是可分離的,他們可以具有各自的生命周期,部分可以屬于多個(gè)整體對(duì)象穴张,也可以為多個(gè)整體對(duì)象共享细燎。
不同于關(guān)聯(lián)關(guān)系的平等地位,聚合關(guān)系中兩個(gè)類(lèi)的地位是不平等皂甘。
組合
組合也是關(guān)聯(lián)關(guān)系的一種特例玻驻,他體現(xiàn)的是一種contains-a的關(guān)系,這種關(guān)系比聚合更強(qiáng)偿枕,也稱(chē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ú)存在桃纯。
只看代碼酷誓,你是無(wú)法區(qū)分關(guān)聯(lián),聚合和組合的态坦,具體是哪一種關(guān)系盐数,只能從語(yǔ)義級(jí)別來(lái)區(qū)分。
同樣伞梯,組合關(guān)系中玫氢,兩個(gè)類(lèi)額關(guān)系也是不平等的。
組合谜诫,聚合和繼承
依賴(lài)關(guān)系是每一個(gè)java程序都離不開(kāi)的漾峡,所以就不單獨(dú)討論了,普通的關(guān)聯(lián)關(guān)系也沒(méi)有什么特殊的地方喻旷,下面我們重點(diǎn)研究一下組合生逸,聚合和繼承。
聚合與組合
聚合與組合都是一種關(guān)聯(lián)關(guān)系且预,只是額外具有整體-部分的意義槽袄。
-
部件的生命周期不同
聚合關(guān)系中,整件不會(huì)擁有部件的生命周期锋谐,所以整件刪除時(shí)遍尺,部件不會(huì)被刪除。再者涮拗,多個(gè)整件可以共享同一個(gè)部件乾戏。
組合關(guān)系中迂苛,整件擁有部件的生命周期,所以整件刪除時(shí)鼓择,部件一定會(huì)跟著刪除灾部。而且,多個(gè)整件不可以同時(shí)間共享同一個(gè)部件惯退。
這個(gè)區(qū)別可以用來(lái)區(qū)分某個(gè)關(guān)聯(lián)關(guān)系到底是組合還是聚合赌髓。兩個(gè)類(lèi)生命周期不同步,則是聚合關(guān)系催跪,生命周期同步就是組合關(guān)系锁蠕。
-
聚合關(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)系通危,所以說(shuō)組合是【has-a】關(guān)系也是正確的铸豁。
組合和繼承
這個(gè)才是本文的重點(diǎn)。
學(xué)過(guò)設(shè)計(jì)模式的都知道菊碟,要“少用繼承节芥,多用組合”,這究竟是為什么呢逆害?
我們先來(lái)看一下組合和繼承各自的優(yōu)缺點(diǎn):
組合和繼承的優(yōu)缺點(diǎn)
組合
優(yōu)點(diǎn):
- 不破壞封裝头镊,整體類(lèi)與局部類(lèi)之間松耦合,彼此相對(duì)獨(dú)立
- 具有較好的可擴(kuò)展性
- 支持動(dòng)態(tài)組合魄幕。在運(yùn)行時(shí)相艇,整體對(duì)象可以選擇不同類(lèi)型的局部對(duì)象
- 整體類(lèi)可以對(duì)局部類(lèi)進(jìn)行包裝,封裝局部類(lèi)的接口纯陨,提供新的接口
缺點(diǎn):
- 整體類(lèi)不能自動(dòng)獲得和局部類(lèi)同樣的接口
- 創(chuàng)建整體類(lèi)的對(duì)象時(shí)坛芽,需要?jiǎng)?chuàng)建所有局部類(lèi)的對(duì)象
缺點(diǎn)分析:
1、整體類(lèi)不能自動(dòng)獲得和局部類(lèi)同樣的接口
如果父類(lèi)的方法子類(lèi)中幾乎都要暴露出去队丝,這時(shí)可能會(huì)覺(jué)得使用組合很不方便靡馁,使用繼承似乎更簡(jiǎn)單方便欲鹏。但從另一個(gè)角度講机久,實(shí)際上也許子類(lèi)中并不需要暴露這些方法,客戶端組合應(yīng)用就可以了赔嚎。所以上邊推薦不要繼承那些不是為了繼承而設(shè)計(jì)的類(lèi)膘盖,一般為了繼承而設(shè)計(jì)的類(lèi)都是抽象類(lèi)胧弛。
2、創(chuàng)建整體類(lèi)的對(duì)象時(shí)侠畔,需要?jiǎng)?chuàng)建所有局部類(lèi)的對(duì)象
這個(gè)可能沒(méi)什么更好的辦法结缚,但在實(shí)際應(yīng)用中并沒(méi)有多出多少代碼。
繼承
優(yōu)點(diǎn):
- 子類(lèi)能自動(dòng)繼承父類(lèi)的接口
- 創(chuàng)建子類(lèi)的對(duì)象時(shí)软棺,無(wú)須創(chuàng)建父類(lèi)的對(duì)象
缺點(diǎn):
- 破壞封裝红竭,子類(lèi)與父類(lèi)之間緊密耦合,子類(lèi)依賴(lài)于父類(lèi)的實(shí)現(xiàn)喘落,子類(lèi)缺乏獨(dú)立性
- 支持?jǐn)U展茵宪,但是往往以增加系統(tǒng)結(jié)構(gòu)的復(fù)雜度為代價(jià)
- 不支持動(dòng)態(tài)繼承。在運(yùn)行時(shí)瘦棋,子類(lèi)無(wú)法選擇不同的父類(lèi)
- 子類(lèi)不能改變父類(lèi)的接口
缺點(diǎn)分析:
1稀火、為什么繼承破壞封裝性?
鴨子中不想要“飛”的方法赌朋,但因?yàn)槔^承無(wú)法封裝這個(gè)無(wú)用的“飛”方法 凰狞。
2、為什么繼承緊耦合:
當(dāng)作為父類(lèi)的BaseTable中感覺(jué)Insert這個(gè)名字不合適時(shí)沛慢,如果希望將其修改成Create方法赡若,那使用了子類(lèi)對(duì)象Insert方法將會(huì)編譯出錯(cuò),可能你會(huì)覺(jué)得這改起來(lái)還算容易团甲,因?yàn)橛兄貥?gòu)工具一下子就好了并且編譯錯(cuò)誤改起來(lái)很容易朴上。但如果BaseTable和子類(lèi)在不同的程序集中蚤霞,維護(hù)的人員不同,BaseTable程序集升級(jí),那本來(lái)能用的代碼忽然不能用了勋桶,這還是很難讓人接受的
3、為什么繼承擴(kuò)展起來(lái)比較復(fù)雜
當(dāng)圖書(shū)和數(shù)碼的算稅方式和數(shù)碼產(chǎn)品一樣時(shí)臼氨,而消費(fèi)類(lèi)產(chǎn)品的算稅方式是另一樣時(shí),如果采用繼承方案可能會(huì)演變成如下方式:
這樣如果產(chǎn)品繼續(xù)增加集乔,算稅方式繼續(xù)增加,那繼承的層次會(huì)非常復(fù)雜尤溜,而且很難控制,而使用組合就能很好的解決這個(gè)問(wèn)題
4汗唱、繼承不能支持動(dòng)態(tài)繼承
這個(gè)其實(shí)很好理解宫莱,因?yàn)槔^承是編譯期就決定下來(lái)的哩罪,無(wú)法在運(yùn)行時(shí)改變巡验,如3例中,如果用戶需要根據(jù)當(dāng)?shù)氐那闆r選擇計(jì)稅方式碘耳,使用繼承就解決不了显设,而使用組合結(jié)合反射就能很好的解決辛辨。
5、為什么繼承绞蹦,子類(lèi)不能改變父類(lèi)接口
如2中的圖榜旦,子類(lèi)中覺(jué)得Insert方法不合適,希望使用Create方法澡屡,因?yàn)槔^承的原因無(wú)法改變
組合與繼承的區(qū)別和聯(lián)系
在繼承結(jié)構(gòu)中咐旧,父類(lèi)的內(nèi)部細(xì)節(jié)對(duì)于子類(lèi)是可見(jiàn)的铣墨。所以我們通常也可以說(shuō)通過(guò)繼承的代碼復(fù)用是一種 白盒式代碼復(fù)用。(如果基類(lèi)的實(shí)現(xiàn)發(fā)生改變伊约,那么派生類(lèi)的實(shí)現(xiàn)也將隨之改變屡律。這樣就導(dǎo)致了子類(lèi)行為的不可預(yù)知性)
組合是通過(guò)對(duì)現(xiàn)有的對(duì)象進(jìn)行拼裝(組合)產(chǎn)生新的、更復(fù)雜的功能搏讶。因?yàn)樵趯?duì)象之間霍殴,各自的內(nèi)部細(xì)節(jié)是不可見(jiàn)的,所以我們也說(shuō)這種方式的代碼復(fù)用是黑盒式代碼復(fù)用 妒蔚。(因?yàn)榻M合中一般都定義一個(gè)類(lèi)型,所以在編譯期根本不知道具體會(huì)調(diào)用哪個(gè)實(shí)現(xiàn)類(lèi)的方法)
繼承在寫(xiě)代碼的時(shí)候就要指名具體繼承哪個(gè)類(lèi)絮蒿,所以叁鉴,在編譯期就確定了關(guān)系佛寿。(從基類(lèi)繼承來(lái)的實(shí)現(xiàn)是無(wú)法在運(yùn)行期動(dòng)態(tài)改變的冀泻,因此降低了應(yīng)用的靈活性。)
組合胳施,在寫(xiě)代碼的時(shí)候可以采用面向接口編程肢专。所以,類(lèi)的組合關(guān)系一般在運(yùn)行期確定椿胯。
組合(has-a)關(guān)系可以顯式地獲得被包含類(lèi)(繼承中稱(chēng)為父類(lèi))的對(duì)象剃根,而繼承(is-a)則是隱式地獲得父類(lèi)的對(duì)象狈醉,被包含類(lèi)和父類(lèi)對(duì)應(yīng),而組合外部類(lèi)和子類(lèi)對(duì)應(yīng)娱两。
組合是在組合類(lèi)和被包含類(lèi)之間的一種松耦合關(guān)系金吗,而繼承則是父類(lèi)和子類(lèi)之間的一種緊耦合關(guān)系。
當(dāng)選擇使用組合關(guān)系時(shí)旱物,在組合類(lèi)中包含了外部類(lèi)的對(duì)象卫袒,組合類(lèi)可以調(diào)用外部類(lèi)必須的方法夕凝,而使用繼承關(guān)系時(shí)户秤,父類(lèi)的所有方法和變量都被子類(lèi)無(wú)條件繼承逮矛,子類(lèi)不能選擇须鼎。
最重要的一點(diǎn),使用繼承關(guān)系時(shí)汞窗,可以實(shí)現(xiàn)類(lèi)型的回溯赡译,即用父類(lèi)變量引用子類(lèi)對(duì)象,這樣便可以實(shí)現(xiàn)多態(tài)裹唆,而組合沒(méi)有這個(gè)特性综看。
還有一點(diǎn)需要注意红碑,如果你確定復(fù)用另外一個(gè)類(lèi)的方法永遠(yuǎn)不需要改變時(shí),應(yīng)該使用組合羡鸥,因?yàn)榻M合只是簡(jiǎn)單地復(fù)用被包含類(lèi)的接口忠寻,而繼承除了復(fù)用父類(lèi)的接口外奕剃,它甚至還可以覆蓋這些接口,修改父類(lèi)接口的默認(rèn)實(shí)現(xiàn)柿顶,這個(gè)特性是組合所不具有的操软。
從邏輯上看,組合最主要地體現(xiàn)的是一種整體和部分的思想家乘,例如在電腦類(lèi)是由內(nèi)存類(lèi)仁锯,CPU類(lèi),硬盤(pán)類(lèi)等等組成的涯呻,而繼承則體現(xiàn)的是一種可以回溯的父子關(guān)系腻要,子類(lèi)也是父類(lèi)的一個(gè)對(duì)象涝登。
這兩者的區(qū)別主要體現(xiàn)在類(lèi)的抽象階段胀滚,在分析類(lèi)之間的關(guān)系時(shí)就應(yīng)該確定是采用組合還是采用繼承。
引用網(wǎng)友的一句很經(jīng)典的話應(yīng)該更能讓大家分清繼承和組合的區(qū)別:組合可以被說(shuō)成“我請(qǐng)了個(gè)老頭在我家里干活” 顷编,繼承則是“我父親在家里幫我干活"剑刑。
**繼承還是組合施掏? **
首先它們都是實(shí)現(xiàn)系統(tǒng)功能重用,代碼復(fù)用的最常用的有效的設(shè)計(jì)技巧素挽,都是在設(shè)計(jì)模式中的基礎(chǔ)結(jié)構(gòu)狸驳。
很多人都知道面向?qū)ο笾杏幸粋€(gè)比較重要的原則『多用組合耙箍、少用繼承』或者說(shuō)『組合優(yōu)于繼承』。從前面的介紹已經(jīng)優(yōu)缺點(diǎn)對(duì)比中也可以看出窗慎,組合確實(shí)比繼承更加靈活,也更有助于代碼維護(hù)峦失。
所以术吗,建議在同樣可行的情況下较屿,優(yōu)先使用組合而不是繼承。因?yàn)榻M合更安全购啄,更簡(jiǎn)單嘱么,更靈活曼振,更高效。
注意映胁,并不是說(shuō)繼承就一點(diǎn)用都沒(méi)有了甲雅,前面說(shuō)的是【在同樣可行的情況下】务荆。有一些場(chǎng)景還是需要使用繼承的,或者是更適合使用繼承娱据。
繼承要慎用盅惜,其使用場(chǎng)合僅限于你確信使用該技術(shù)有效的情況抒寂。一個(gè)判斷方法是,問(wèn)一問(wèn)自己是否需要從新類(lèi)向基類(lèi)進(jìn)行向上轉(zhuǎn)型郊愧。如果是必須的,則繼承是必要的眠寿。反之則應(yīng)該好好考慮是否需要繼承焦蘑。
只有當(dāng)子類(lèi)真正是超類(lèi)的子類(lèi)型時(shí)例嘱,才適合用繼承。換句話說(shuō)奢浑,對(duì)于兩個(gè)類(lèi)A和B间学,只有當(dāng)兩者之間確實(shí)存在 is-a 關(guān)系的時(shí)候低葫,類(lèi)B才應(yīng)該繼承類(lèi)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的過(guò)程中得到了大量的強(qiáng)調(diào)钢拧,但并不意味著應(yīng)該盡可能地到處使用它源内。相反份殿,使用它時(shí)要特別慎重。
只有在清楚知道繼承在所有方法中最有效的前提下颂斜,才可考慮它沃疮。 繼承最大的優(yōu)點(diǎn)就是擴(kuò)展簡(jiǎn)單,但大多數(shù)缺點(diǎn)都很致命糯彬,但是因?yàn)檫@個(gè)擴(kuò)展簡(jiǎn)單的優(yōu)點(diǎn)太明顯了撩扒,很多人并不深入思考吨些,所以造成了太多問(wèn)題。
最后泉手,總結(jié)一下:
1斩萌、精心設(shè)計(jì)專(zhuān)門(mén)用于被繼承的類(lèi)屏轰,繼承樹(shù)的抽象層應(yīng)該比較穩(wěn)定霎苗,一般不要多于三層。
2内狸、對(duì)于不是專(zhuān)門(mén)用于被繼承的類(lèi)厘擂,禁止其被繼承刽严。
3、優(yōu)先考慮用組合關(guān)系來(lái)提高代碼的可重用性倔既。
4渤涌、子類(lèi)是一種特殊的類(lèi)型把还,而不只是父類(lèi)的一個(gè)角色
5、子類(lèi)擴(kuò)展安皱,而不是覆蓋或者使父類(lèi)的功能失效
沒(méi)錯(cuò)酌伊,寫(xiě)了這么多,就是想說(shuō): 請(qǐng)慎重使用繼承虹脯,除非你確定非用繼承不可循集!
這篇文章寫(xiě)得比較粗糙蔗草,因?yàn)閷?xiě)文章的時(shí)候一直在拉肚子咒精。。奸例。以后還會(huì)做一些修改,暫時(shí)先這樣湖蜕。如果文中有錯(cuò)誤或者有更好的解釋?zhuān)瑲g迎給我留言宋列。我也只是一個(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.reibang.com/p/edca9422b203
轉(zhuǎn)載請(qǐng)注明出處熙含。
看完點(diǎn)個(gè)贊唄怎静。