java中類與類之間的關系
大部分的初學者只知道java中兩個類之間可以是繼承與被繼承的關系,可是事實上,類之間的關系大體上存在五種—繼承(實現)、依賴、關聯、聚合、組合。
接下來,簡單的分析一下這些關系。
繼承(實現)
對於類來說,這種關系叫做繼承,對於接口來說,這種關系叫做實現。繼承上一篇文章已經詳細的講解過了,至於實現,我想大家也都知道是怎么回事,由於后面要專門講接口,所以這里就先不說了。繼承是一種“is-a”關系。
依賴
依賴簡單的理解,就是一個類A中的方法使用到了另一個類B。
這種使用關系是具有偶然性的、臨時性的、非常弱的,但是B類的變化會影響到A。
比如說,我用筆寫字,首先需要一個類來代表我自己,然后需要一個類來代表一支筆,最后,‘我’要調用‘筆’里的方法來寫字,用代碼實現一下:
public class Pen { public void write(){ System.out.println("use pen to write"); } } public class Me { public void write(Pen pen){//這里,pen作為Me類方法的參數。 Me類依賴Pen類 pen.write(); } }
看到這大家都懂了,因為這種代碼你每天都會寫。現在你知道了,這就是一種類與類之間的關系,叫做依賴。
這種關系是一種很弱的關系,但是pen類的改變,有可能會影響到Me類的結果,比如我把pen類write方法的方法體改了,me中再調用就會得到不同的結果。
一般而言,依賴關系在Java中體現為局域變量、方法的形參,或者對靜態方法的調用。
關聯
關聯體現的是兩個類、或者類與接口之間語義級別的一種強依賴關系。
這種關系比依賴更強、不存在依賴關系的偶然性、關系也不是臨時性的,一般是長期性的,而且雙方的關系一般是平等的、關聯可以是單向、雙向的。
看下面這段代碼:
// pen 還是上面的pen public class You { private Pen pen; // 讓pen成為you的類屬性 public You(Pen p){ this.pen = p; } public void write(){ pen.write(); } }
被關聯類B以類屬性的形式出現在關聯類A中,或者關聯類A引用了一個類型為被關聯類B的全局變量的這種關系,就叫關聯關系。
在Java中,關聯關系一般使用成員變量來實現。
聚合
聚合是關聯關系的一種特例,他體現的是整體與部分、擁有的關系,即has-a的關系
看下面一段代碼:
public class Family { private List<Child> children; //一個家庭里有許多孩子 // ... }
在代碼層面,聚合和關聯關系是一致的,只能從語義級別來區分。普通的關聯關系中,a類和b類沒有必然的聯系,而聚合中,需要b類是a類的一部分,是一種”has-a“的關系,即 a has-a b; 比如家庭有孩子,屋子里有空調。
但是,has 不是 must has,a可以有b,也可以沒有。a是整體,b是部分,整體與部分之間是可分離的,他們可以具有各自的生命周期,部分可以屬於多個整體對象,也可以為多個整體對象共享。
不同於關聯關系的平等地位,聚合關系中兩個類的地位是不平等。
組合
組合也是關聯關系的一種特例,他體現的是一種contains-a的關系,這種關系比聚合更強,也稱為強聚合。
先看一段代碼:
public class Person { private Eye eye = new Eye(); //一個人有鼻子有眼睛 private Nose nose = new Nose(); // .... }
組合同樣體現整體與部分間的關系,但此時整體與部分是不可分的,整體的生命周期結束也就意味着部分的生命周期結束。
就像你有鼻子有眼睛,如果你一不小心結束了生命周期,鼻子和眼睛的生命周期也會結束,而且,鼻子和眼睛不能脫離你單獨存在。
只看代碼,你是無法區分關聯,聚合和組合的,具體是哪一種關系,只能從語義級別來區分。
同樣,組合關系中,兩個類關系也是不平等的。
組合,聚合和繼承
依賴關系是每一個java程序都離不開的,所以就不單獨討論了,普通的關聯關系也沒有什么特殊的地方,下面我們重點研究一下組合,聚合和繼承。
聚合與組合
聚合與組合都是一種關聯關系,只是額外具有整體-部分的意義。
部件的生命周期不同
聚合關系中,整件不會擁有部件的生命周期,所以整件刪除時,部件不會被刪除。再者,多個整件可以共享同一個部件。
組合關系中,整件擁有部件的生命周期,所以整件刪除時,部件一定會跟着刪除。而且,多個整件不可以同時間共享同一個部件。
這個區別可以用來區分某個關聯關系到底是組合還是聚合。兩個類生命周期不同步,則是聚合關系,生命周期同步就是組合關系。
聚合關系是【has-a】關系,組合關系是【contains-a】關系。
平時我們只討論組合和繼承的時候,認為組合是【has-a 】關系,而事實上,聚合才是真正的【has-a】關系,組合是更深層次的【contains-a】關系。
由於【contains-a】關系是一種更深的【has-a】關系,所以說組合是【has-a】關系也是正確的。
組合和繼承
這個才是本文的重點。
學過設計模式的都知道,要“少用繼承,多用組合”,這究竟是為什么呢?
我們先來看一下組合和繼承各自的優缺點:
組合和繼承的優缺點
組合
優點:
- 不破壞封裝,整體類與局部類之間松耦合,彼此相對獨立 - 具有較好的可擴展性 - 支持動態組合。在運行時,整體對象可以選擇不同類型的局部對象 - 整體類可以對局部類進行包裝,封裝局部類的接口,提供新的接口
缺點:
- 整體類不能自動獲得和局部類同樣的接口
- 創建整體類的對象時,需要創建所有局部類的對象
缺點分析:
1、整體類不能自動獲得和局部類同樣的接口
如果父類的方法子類中幾乎都要暴露出去,這時可能會覺得使用組合很不方便,使用繼承似乎更簡單方便。
但從另一個角度講,實際上也許子類中並不需要暴露這些方法,客戶端組合應用就可以了。
所以上邊推薦不要繼承那些不是為了繼承而設計的類,一般為了繼承而設計的類都是抽象類。
創建整體類的對象時,需要創建所有局部類的對象
繼承
優點
- 子類能自動繼承父類的接口
- 創建子類的對象時,無須創建父類的對象
缺點:
- 破壞封裝,子類與父類之間緊密耦合,子類依賴於父類的實現,子類缺乏獨立性 - 支持擴展,但是往往以增加系統結構的復雜度為代價 - 不支持動態繼承。在運行時,子類無法選擇不同的父類 - 子類不能改變父類的接口
缺點分析:
1、為什么繼承破壞封裝性?
鴨子中不想要“飛”的方法,但因為繼承無法封裝這個無用的“飛”方法 。
2、為什么繼承緊耦合:
當作為父類的BaseTable中感覺Insert這個名字不合適時,如果希望將其修改成Create方法,那使用了子類對象Insert方法將會編譯出錯,可能你會覺得這改起來還算容易,因為有重構工具一下子就好了並且編譯錯誤改起來很容易。但如果BaseTable和子類在不同的程序集中,維護的人員不同,BaseTable程序集升級,那本來能用的代碼忽然不能用了,這還是很難讓人接受的
3、為什么繼承擴展起來比較復雜
當圖書和數碼的算稅方式和數碼產品一樣時,而消費類產品的算稅方式是另一樣時,如果采用繼承方案可能會演變成如下方式:
這樣如果產品繼續增加,算稅方式繼續增加,那繼承的層次會非常復雜,而且很難控制,而使用組合就能很好的解決這個問題
4、繼承不能支持動態繼承
這個其實很好理解,因為繼承是編譯期就決定下來的,無法在運行時改變,如3例中,如果用戶需要根據當地的情況選擇計稅方式,使用繼承就解決不了,而使用組合結合反射就能很好的解決。
5、為什么繼承,子類不能改變父類接口
如2中的圖,子類中覺得Insert方法不合適,希望使用Create方法,因為繼承的原因無法改變
組合與繼承的區別和聯系
在繼承結構中,父類的內部細節對於子類是可見的。所以我們通常也可以說通過繼承的代碼復用是一種 白盒式代碼復用。(如果基類的實現發生改變,那么派生類的實現也將隨之改變。這樣就導致了子類行為的不可預知性)
組合是通過對現有的對象進行拼裝(組合)產生新的、更復雜的功能。因為在對象之間,各自的內部細節是不可見的,所以我們也說這種方式的代碼復用是黑盒式代碼復用 。(因為組合中一般都定義一個類型,所以在編譯期根本不知道具體會調用哪個實現類的方法)
繼承在寫代碼的時候就要指名具體繼承哪個類,所以,在編譯期就確定了關系。(從基類繼承來的實現是無法在運行期動態改變的,因此降低了應用的靈活性。)
組合,在寫代碼的時候可以采用面向接口編程。所以,類的組合關系一般在運行期確定。
組合(has-a)關系可以顯式地獲得被包含類(繼承中稱為父類)的對象,而繼承(is-a)則是隱式地獲得父類的對象,被包含類和父類對應,而組合外部類和子類對應。
組合是在組合類和被包含類之間的一種松耦合關系,而繼承則是父類和子類之間的一種緊耦合關系。
當選擇使用組合關系時,在組合類中包含了外部類的對象,組合類可以調用外部類必須的方法,而使用繼承關系時,父類的所有方法和變量都被子類無條件繼承,子類不能選擇。
最重要的一點,使用繼承關系時,可以實現類型的回溯,即用父類變量引用子類對象,這樣便可以實現多態,而組合沒有這個特性。
還有一點需要注意,如果你確定復用另外一個類的方法永遠不需要改變時,應該使用組合,因為組合只是簡單地復用被包含類的接口,而繼承除了復用父類的接口外,它甚至還可以覆蓋這些接口,修改父類接口的默認實現,這個特性是組合所不具有的。
從邏輯上看,組合最主要地體現的是一種整體和部分的思想,例如在電腦類是由內存類,CPU類,硬盤類等等組成的,而繼承則體現的是一種可以回溯的父子關系,子類也是父類的一個對象。
這兩者的區別主要體現在類的抽象階段,在分析類之間的關系時就應該確定是采用組合還是采用繼承。
引用網友的一句很經典的話應該更能讓大家分清繼承和組合的區別:組合可以被說成“我請了個老頭在我家里干活” ,繼承則是“我父親在家里幫我干活”。
繼承還是組合?
首先它們都是實現系統功能重用,代碼復用的最常用的有效的設計技巧,都是在設計模式中的基礎結構。
很多人都知道面向對象中有一個比較重要的原則『多用組合、少用繼承』或者說『組合優於繼承』。從前面的介紹已經優缺點對比中也可以看出,組合確實比繼承更加靈活,也更有助於代碼維護。
所以,建議在同樣可行的情況下,優先使用組合而不是繼承。因為組合更安全,更簡單,更靈活,更高效。
注意,並不是說繼承就一點用都沒有了,前面說的是【在同樣可行的情況下】。有一些場景還是需要使用繼承的,或者是更適合使用繼承。
繼承要慎用,其使用場合僅限於你確信使用該技術有效的情況。一個判斷方法是,問一問自己是否需要從新類向基類進行向上轉型。如果是必須的,則繼承是必要的。反之則應該好好考慮是否需要繼承。
只有當子類真正是超類的子類型時,才適合用繼承。換句話說,對於兩個類A和B,只有當兩者之間確實存在 is-a 關系的時候,類B才應該繼承類A。
發生多態的三個前提條件:
-
繼承。多態發生一定要子類和父類之間。
-
覆蓋。子類覆蓋了父類的方法。
-
聲明的變量類型是父類類型,但實例則指向子類實例。
參考文章: https://blog.csdn.net/qq_31655965/article/details/54645220