Java中的繼承與組合
本文主要說明Java中繼承與組合的概念,以及它們之間的聯絡與區別。首先文章會給出一小段程式碼示例,用於展示到底什麼是繼承。然後演示如何通過“組合”來改進這種繼承的設計機制。最後總結這兩者的應用場景,即到底應該選擇繼承還是組合。
1、繼承
假設我們有一個名為Insect(昆蟲)的類,這個類包含兩個方法:1)移動move(); 2)攻擊attack()。
程式碼如下:
class Insect { private int size; private String color; public Insect(int size, String color) { this.size = size; this.color = color; } public int getSize() { return size; } public void setSize(int size) { this.size = size; } public String getColor() { return color; } public void setColor(String color) { this.color = color; } public void move() { System.out.println("Move"); } public void attack() { move(); //假設昆蟲在攻擊前必須要先移動一次 System.out.println("Attack"); } }
現在,你想要定義一個名為Bee(蜜蜂)的類。Bee(蜜蜂)是Insect(昆蟲)的一種,但實現了不同於Insect(昆蟲)的attack()和move方法。這時候我們可以用繼承的設計機制來實現Bee類,就像下面的程式碼一樣:
class Bee extends Insect { public Bee(int size, String color) { super(size, color); } public void move() { System.out.println("Fly"); } public void attack() { move(); super.attack(); } }
public class InheritanceVSComposition { public static void main(String[] args) { Insect i = new Bee(1, "red"); i.attack(); } }
InheritanceVSComposition作為一個測試類,在其main方法中生成了一個Bee類的例項,並賦值給Insect型別的引用變數 i。所以呼叫i的attack方法時,對應的是Bee類例項的attack方法,也就是呼叫了Bee類的attack方法。
類的繼承結構圖如下,非常簡單:
輸出:
Fly Fly Attack
Fly被列印了兩次,也就是說move方法被呼叫了兩次。但按理來講,move方法只應當被呼叫一次,因為無論是昆蟲還是蜜蜂,一次攻擊前只移動一次。
問題出在子類(即Bee類)的attack方法的過載程式碼中,也就是super.attack()這一句。因為在父類(即Insect類)中,呼叫 attack方法時會先呼叫move方法,所以當子類(Bee)呼叫super.attack()時,相當於也同時呼叫了被過載的move方法(注意是子 類被過載的move方法,而不是父類的move方法)。
為了解決這個問題,我們可以採取以下辦法:
- 刪除子類的attack方法。這麼做會使得子類的attack方法的實現完全依賴於父類對於該方法的實現(因為子類繼承了父類的attack方法)。如果 父類的attack方法不受控制而產生了變更。比如說,父類的attack方法中呼叫了另外的move方法,那麼子類的attack方法也會產生相應的變 化,這是一種很糟糕的封裝。
- 也可以重寫子類的attack方法,像下面這樣:
public void attack() { move(); System.out.println("Attack"); }
這樣保證了結果的正確性,因為子類的attack方法不再依賴於父類。但是,子類attack方法的程式碼與父類產生了重複(重複的attack方法會使得很多事情變得複雜,不僅僅是多列印了一條輸出語句)。所以第二種辦法也不行,它不符合軟體工程中關於重用的思想。
如此看來,繼承機制是有缺點的:子類依賴於父類的實現細節,如果父類產生了變更,子類的後果將不堪設想。
2、組合
在上面的例子中,可以用組合的機制來替代繼承。我們先看一下運用組合如何實現。
attack這一功能不再是一個方法,而是被抽象為一個介面。
interface Attack { public void move(); public void attack(); }
通過對Attack介面的實現,就可以在實現類當中定義不同型別的attack。
class AttackImpl implements Attack { private String move; private String attack; public AttackImpl(String move, String attack) { this.move = move; this.attack = attack; } @Override public void move() { System.out.println(move); } @Override public void attack() { move(); System.out.println(attack); } }
因為attack功能已經被抽象為一個介面,所以Insect類不再需要有attack方法。
class Insect { private int size; private String color; public Insect(int size, String color) { this.size = size; this.color = color; } public int getSize() { return size; } public void setSize(int size) { this.size = size; } public String getColor() { return color; } public void setColor(String color) { this.color = color; } }
Bee類一種Insect類,它具有attack的功能,所以它實現了attack介面:
// 這個封裝類封裝了一個Attack型別的物件 class Bee extends Insect implements Attack { private Attack attack; public Bee(int size, String color, Attack attack) { super(size, color); this.attack = attack; } public void move() { attack.move(); } public void attack() { attack.attack(); } }
類圖:
測試類程式碼,將AttackImpl的例項作為Attack型別的引數傳給Bee類的建構函式:
public class InheritanceVSComposition2 { public static void main(String[] args) { Bee a = new Bee(1, "black", new AttackImpl("fly", "move")); a.attack(); // if you need another implementation of move() // there is no need to change Insect, we can quickly use new method to attack Bee b = new Bee(1, "black", new AttackImpl("fly", "sting")); b.attack(); } }
fly move fly sting
3、什麼時候該用繼承,什麼時候該用組合?
以下兩條原則說明了應該如何選擇繼承與組合:
- 如果存在一種IS-A的關係(比如Bee“是一個”Insect),並且一個類需要向另一個類暴露所有的方法介面,那麼更應該用繼承的機制。
- 如果存在一種HAS-A的關係(比如Bee“有一個”attack功能),那麼更應該運用組合。
總結來說,繼承和組合都有他們的用處。只有充分理解各物件和功能之間的關係,才能充分發揮這兩種機制各自的優點。
參考:
- Bloch, Joshua. Effective Java. Pearson Education India, 2008.
- http://stackoverflow.com/questions/49002/prefer-composition-over-inheritance
- http://www.javaworld.com/article/2076814/core-java/inheritance-versus-composition–which-one-should-you-choose-.html
相關文章
- 繼承與組合繼承
- 【Java】繼承、抽象、組合Java繼承抽象
- 深入理解java中的組合和繼承Java繼承
- JavaScript中的繼承和組合JavaScript繼承
- 類的組合與繼承——作業繼承
- 菜鳥譯文(一)——Java中的繼承和組合Java繼承
- prefer 組合 to 繼承繼承
- YTU-OJ-繼承與組合繼承
- 組合優於繼承繼承
- 組合思維與繼承思維的不同繼承
- js 組合繼承詳解JS繼承
- java中的繼承Java繼承
- Docker的組合優於繼承 - frankelDocker繼承
- Java中的類繼承與多型Java繼承多型
- java中繼承Java中繼繼承
- Java的類與繼承Java繼承
- javascript組合繼承的基本原理JavaScript繼承
- Java:類與繼承Java繼承
- python物件導向的繼承-組合-02Python物件繼承
- 【設計模式】如何用組合替代繼承設計模式繼承
- java繼承與多型Java繼承多型
- C++ 多級繼承與多重繼承:程式碼組織與靈活性的平衡C++繼承
- Java 繼承與多型:程式碼重用與靈活性的巧妙結合Java繼承多型
- Java的繼承Java繼承
- Java程式碼塊與Java繼承Java繼承
- 多繼承 與 多重繼承繼承
- JS中的繼承與原型鏈JS繼承原型
- Java 自學 - 介面與繼承 介面Java繼承
- Java中介面與繼承的區別Java繼承
- Java繼承Java繼承
- JAVA中的註解可以繼承嗎?Java繼承
- java之繼承中的構造方法Java繼承構造方法
- Java繼承的使用Java繼承
- 實驗4 類的組合、繼承、模板類、標準庫繼承
- [許可權設計]組的繼承:需要從多個父組繼承嗎?繼承
- Java 繼承與多型實驗Java繼承多型
- C++中公有繼承、保護繼承、私有繼承的區別C++繼承
- JavaScript中的繼承JavaScript繼承