組合(has-a 關系)
我們已經嘗試去定義類。定義類,就是新建了一種類型(type)。有了類,我們接着構造相應類型的對象。更進一步,每個類型還應該有一個清晰的接口(interface),供用戶使用。
我們可以在一個新類的定義中使用其他對象。這就是組合(composition)。組合是在Java中實現程序復用(reusibility)的基本手段之一。
組合:一個對象是另一個對象的數據成員。
【例子1 code】
package com.gta.testoop.inherit; /**@Description: 測試組合 * @date 2016-2-1 上午10:30:57 */ //一個源文件可以定義多個類 //動物Animal類 public class Animal2 { String eye; public void run(){ System.out.println("跑跑"); } public void eat(){ System.out.println("吃吃"); } } //哺乳動物Mammal類 class Mammal2 { Animal2 animal2;//作為Mammal2的一個屬性 public void taiSheng(){ System.out.println("我是胎生"); } } class Bird { Animal2 animal2=new Animal2() ; public void run(){ animal2.run(); System.out.println("我是一個小小小鳥,想要飛的更高"); } public void eggSheng(){ System.out.println("我是卵生"); } }
測試類:
package com.gta.testoop.inherit; /**@Description: 測試組合 * @date 2016-2-2 上午9:34:11 */ public class TestComposition { public static void main(String[] args) { Bird b=new Bird(); b.run(); b.animal2.eat(); } }
執行結果:

跑跑
我是一個小小小鳥,想要飛的更高
吃吃
【例子2 code】充電筒例子;
一個充電電筒中的電池、LED燈、按鈕…… 都可以是一個對象。我們可以定義一個Battery類來定義和產生電池對象。而在充電電筒的類定義中,可以用一個電池對象作為其數據成員,來代表電池部分的狀態。
我們下面定義一個Battery類,並用power來表示其電量。一個Battery的可以充電(chargeBattery)和使用(useBattery)。我們在隨后的Torch類定義中使用Battery類型的對象作為數據成員:
class Battery { public void chargeBattery(double p) { if (this.power < 1.) { this.power = this.power + p; } } public boolean useBattery(double p) { if (this.power >= p) { this.power = this.power - p; return true; } else { this.power = 0.0; return false; } } private double power = 0.0; } class Torch { /** * 10% power per hour use * warning when out of power */ public void turnOn(int hours) { boolean usable; usable = this.theBattery.useBattery( hours*0.1 ); if (usable != true) { System.out.println("No more usable, must charge!"); } } /** * 20% power per hour charge */ public void charge(int hours) { this.theBattery.chargeBattery( hours*0.2 ); } /** * composition */ private Battery theBattery = new Battery(); }
上面的new為theBattery對象分配內存,不可或缺。
我們定義Battery類。Torch類使用了一個Battery類型的對象(theBattery)來作為數據成員。在Torch的方法中,我們通過操縱theBattery對象的接口,來實現Battery類所提供的功能(functionality)。
我們說,一個Torch對象擁有(has-a)一個Battery對象。上述關系可以表示成:
has-a: 手電有電池 (注意上面的菱形連線)
通過組合,我們可以復用Battery相關的代碼。假如我們還有其他使用Battery的類,比如手機,計算器,我們都可以將Battery對象組合進去。這樣就不用為每個類單獨編寫相關功能了。
我們可以增加一個Test類,看看實際效果:
public class Test { public static void main(String[] args) { Torch aTorch = new Torch(); System.out.println("Charge: 2 hours"); aTorch.charge(2); System.out.println("First Turn On: 3 hours"); aTorch.turnOn(3); System.out.println("Second Turn On: 3 hours"); aTorch.turnOn(3); } }
執行結果:

Charge: 2 hours First Turn On: 3 hours Second Turn On: 3 hours No more usable, must charge!
我們通過組合來使用了電池對象所提供的功能,比如探測電量是否用盡(根據useBattery()的返回值)。
繼承(is-a) VS 組合(has-a)
【組合和繼承的綜合例子】:
要實現的目標:鳥(Bird)和狼(Wolf)都是動物(Animal),動物都有心跳(beat()),會呼吸(beat()),但是鳥會fly(fly()),狼會奔跑(run()),用java程序實現以上描述。
InheritTest.java 使用繼承方式實現目標
CompositeTest.java 使用組合方式實現目標
1 //InheritTest.java 使用繼承方式實現目標 2 class Animal{ 3 private void beat(){ 4 System.out.println("心臟跳動..."); 5 } 6 public void breath(){ 7 beat(); 8 System.out.println("吸一口氣,呼一口氣,呼吸中..."); 9 } 10 } 11 //繼承Animal,直接復用父類的breath()方法 12 class Bird extends Animal{ 13 //創建子類獨有的方法fly() 14 public void fly(){ 15 System.out.println("我是鳥,我在天空中自由的飛翔..."); 16 } 17 } 18 //繼承Animal,直接復用父類的breath()方法 19 class Wolf extends Animal{ 20 //創建子類獨有的方法run() 21 public void run(){ 22 System.out.println("我是狼,我在草原上快速奔跑..."); 23 } 24 } 25 public class InheritTest{ 26 public static void main(String[] args){ 27 //創建繼承自Animal的Bird對象新實例b 28 Bird b=new Bird(); 29 //新對象實例b可以breath() 30 b.breath(); 31 //新對象實例b可以fly() 32 b.fly(); 33 Wolf w=new Wolf(); 34 w.breath(); 35 w.run(); 36 /* 37 ---------- 運行Java程序 ---------- 38 心臟跳動... 39 吸一口氣,呼一口氣,呼吸中... 40 我是鳥,我在天空中自由的飛翔... 41 心臟跳動... 42 吸一口氣,呼一口氣,呼吸中... 43 我是狼,我在草原上快速奔跑... 44 45 輸出完畢 (耗時 0 秒) - 正常終止 46 */ 47 } 48 } 49 50 //CompositeTest.java 使用組合方式實現目標 51 class Animal{ 52 private void beat(){ 53 System.out.println("心臟跳動..."); 54 } 55 public void breath(){ 56 beat(); 57 System.out.println("吸一口氣,呼一口氣,呼吸中..."); 58 } 59 } 60 class Bird{ 61 //定義一個Animal成員變量,以供組合之用 62 private Animal a; 63 //使用構造函數初始化成員變量 64 public Bird(Animal a){ 65 this.a=a; 66 } 67 //通過調用成員變量的固有方法(a.breath())使新類具有相同的功能(breath()) 68 public void breath(){ 69 a.breath(); 70 } 71 //為新類增加新的方法 72 public void fly(){ 73 System.out.println("我是鳥,我在天空中自由的飛翔..."); 74 } 75 } 76 class Wolf{ 77 private Animal a; 78 public Wolf(Animal a){ 79 this.a=a; 80 } 81 public void breath(){ 82 a.breath(); 83 } 84 public void run(){ 85 System.out.println("我是狼,我在草原上快速奔跑..."); 86 } 87 } 88 public class CompositeTest{ 89 public static void main(String[] args){ 90 //顯式創建被組合的對象實例a1 91 Animal a1=new Animal(); 92 //以a1為基礎組合出新對象實例b 93 Bird b=new Bird(a1); 94 //新對象實例b可以breath() 95 b.breath(); 96 //新對象實例b可以fly() 97 b.fly(); 98 Animal a2=new Animal(); 99 Wolf w=new Wolf(a2); 100 w.breath(); 101 w.run(); 102 /* 103 ---------- 運行Java程序 ---------- 104 心臟跳動... 105 吸一口氣,呼一口氣,呼吸中... 106 我是鳥,我在天空中自由的飛翔... 107 心臟跳動... 108 吸一口氣,呼一口氣,呼吸中... 109 我是狼,我在草原上快速奔跑... 110 111 輸出完畢 (耗時 0 秒) - 正常終止 112 */ 113 } 114 }
總結:
繼承和組合都可以實現代碼的復用。
- "is-a"(是)關系使用繼承!
- "has-a"(擁有)關系使用組合!
最后總結為以下幾點:
1)組合(has-a)關系可以顯式地獲得被包含類(繼承中稱為父類)的對象,而繼承(is-a)則是隱式地獲得父類的對象,被包含類和父類對應,而組合外部類和子類對應。 |
2)組合關系在運行期決定,而繼承關系在編譯期就已經決定了。 |
3)組合是在組合類和被包含類之間的一種松耦合關系,而繼承則是父類和子類之間的一種緊耦合關系。 |
4)當選擇使用組合關系時,在組合類中包含了外部類的對象,組合類可以調用外部類必須的方法,而使用繼承關系時,父類的所有方法和變量都被子類無條件繼承,子類不能選擇。 |
5)最重要的一點,使用繼承關系時,可以實現類型的回溯,即用父類變量引用子類對象,這樣便可以實現多態,而組合沒有這個特性。 |
6)還有一點需要注意,如果你確定復用另外一個類的方法永遠不需要改變時,應該使用組合,因為組合只是簡單地復用被包含類的接口,而繼承除了復用父類的接口外,它甚至還可以覆蓋這些接口,修改父類接口的默認實現,這個特性是組合所不具有的。 |
7)從邏輯上看,組合最主要地體現的是一種整體和部分的思想,例如在電腦類是由內存類,CPU類,硬盤類等等組成的,而繼承則體現的是一種可以回溯的父子關系,子類也是父類的一個對象。 |
8)這兩者的區別主要體現在類的抽象階段,在分析類之間的關系時就應該確定是采用組合還是采用繼承。 |
9)引用網友的一句很經典的話應該更能讓大家分清繼承和組合的區別:組合可以被說成“我請了個老頭在我家里干活” ,繼承則是“我父親在家里幫我干活"。 |