今天我們將探討Java中的多重繼承。不久前,我寫了一些關於繼承、介面和組合的Java文章。在這篇文章中,我們將研究Java的多重繼承,然後比較組合和繼承。
Java中的多重繼承
Java中的多重繼承是指創建一個帶有多個父類的單一類的能力。與其他一些流行的面向對象編程語言(如C++)不同,Java不支持類的多重繼承。Java不支持類的多重繼承,因為它可能導致菱形問題,而且它並沒有提供一些複雜的解決方法,相反,我們可以通過其他更好的方式達到相同的結果。
Java中的鑽石問題
為了更容易理解鑽石問題,讓我們假設在Java中支持多重繼承。在這種情況下,我們可以有如下圖所示的類層次結構。假設SuperClass是一個抽象類聲明了一些方法,而ClassA、ClassB則是具體類。
SuperClass.java
package com.journaldev.inheritance;
public abstract class SuperClass {
public abstract void doSomething();
}
ClassA.java
package com.journaldev.inheritance;
public class ClassA extends SuperClass{
@Override
public void doSomething(){
System.out.println("doSomething implementation of A");
}
//ClassA擁有的方法
public void methodA(){
}
}
ClassB.java
package com.journaldev.inheritance;
public class ClassB extends SuperClass{
@Override
public void doSomething(){
System.out.println("doSomething implementation of B");
}
//ClassB特有的方法
public void methodB(){
}
}
現在假設ClassC的實現如下,它同時擴展了ClassA和ClassB。 ClassC.java
package com.journaldev.inheritance;
// 這只是一個解釋鑽石問題的假設
// 這段代碼不會編譯
public class ClassC extends ClassA, ClassB{
public void test(){
// 調用超類方法
doSomething();
}
}
請注意,test()
方法呼叫了超類 doSomething()
方法。這導致了編譯器不知道要執行哪個超類方法的歧義。由於鑽石形狀的類圖,在 Java 中被稱為鑽石問題。Java 中的鑽石問題是 Java 不支持類的多重繼承的主要原因。注意,上述與多個類繼承相關的問題也可能只涉及三個類,其中所有類至少有一個共同方法。
Java 接口中的多重繼承
您可能已經注意到,我一直在說類不支持多重繼承,但是接口支持。單個接口可以擴展多個接口,下面是一個簡單的示例。InterfaceA.java
package com.journaldev.inheritance;
public interface InterfaceA {
public void doSomething();
}
InterfaceB.java
package com.journaldev.inheritance;
public interface InterfaceB {
public void doSomething();
}
請注意,兩個接口聲明了相同的方法,現在我們可以有一個接口同時擴展這兩個接口,如下所示。InterfaceC.java
package com.journaldev.inheritance;
public interface InterfaceC extends InterfaceA, InterfaceB {
// 在 InterfaceA 和 InterfaceB 中聲明了相同的方法
public void doSomething();
}
這完全沒問題,因為這些接口只是聲明方法,而實際實現將由實現接口的具體類完成。因此,在Java接口中不存在多重繼承的任何模糊性可能性。這就是為什麼Java類可以實現多個接口的原因,就像下面的例子一樣。 InterfacesImpl.java
package com.journaldev.inheritance;
public class InterfacesImpl implements InterfaceA, InterfaceB, InterfaceC {
@Override
public void doSomething() {
System.out.println("doSomething implementation of concrete class");
}
public static void main(String[] args) {
InterfaceA objA = new InterfacesImpl();
InterfaceB objB = new InterfacesImpl();
InterfaceC objC = new InterfacesImpl();
//下面所有的方法調用都將指向相同的具體實現
objA.doSomething();
objB.doSomething();
objC.doSomething();
}
}
你是否注意到,每次我重寫任何超類方法或實現任何接口方法時,我都使用@Override註釋。覆蓋註釋是三個內建的Java註釋之一,我們應該始終在覆蓋任何方法時使用覆蓋註釋。
組合來拯救
那麼,如果我們想要在ClassC
中利用ClassA
函數methodA()
和ClassB
函數methodB()
,該怎麼辦呢?解決方案在於使用組合。這是一個重構過的ClassC版本,它使用組合來利用兩個類的方法,並且還使用了其中一個對象的doSomething()方法。 ClassC.java
package com.journaldev.inheritance;
public class ClassC{
ClassA objA = new ClassA();
ClassB objB = new ClassB();
public void test(){
objA.doSomething();
}
public void methodA(){
objA.methodA();
}
public void methodB(){
objB.methodB();
}
}
組合 vs 繼承
Java 編程的最佳實踐之一是「優先選擇組合而非繼承」。我們將探討一些支持這種方法的方面。
-
假設我們有一個超級類和子類如下:
ClassC.java
package com.journaldev.inheritance; public class ClassC{ public void methodC(){ } }
ClassD.java
package com.journaldev.inheritance; public class ClassD extends ClassC{ public int test(){ return 0; } }
上述代碼編譯並運行正常,但如果ClassC的實現如下所示,會怎麼樣:
ClassC.java
package com.journaldev.inheritance; public class ClassC{ public void methodC(){ } public void test(){ } }
注意,子類中已經存在
test()
方法,但返回類型不同。現在,ClassD將無法編譯,如果您使用任何IDE,它將建議您更改超類或子類中的返回類型。現在想像一下,如果我們有多層級的類繼承,而超類不是我們控制的。我們將不得不改變子類的方法簽名或其名稱以消除編譯錯誤。此外,我們還必須更改所有調用子類方法的地方,因此繼承使我們的代碼變得脆弱。這個問題在組合中永遠不會發生,這使得組合比繼承更受青睞。 -
繼承的另一個問題是我們向客戶端公開了所有的超類方法,如果我們的超類設計不良並存在安全漏洞,則即使我們在實現自己的類時完全小心,也會受到超類不良實現的影響。組合幫助我們對超類方法提供受控訪問,而繼承不提供任何對超類方法的控制,這也是組合優於繼承的主要優勢之一。
-
組合的另一個好處是它提供了方法調用的靈活性。我們上面的
ClassC
實現不是最優的,它提供了與將被調用的方法的編譯時綁定,只需進行最小更改,我們就可以使方法調用靈活並使其動態化。ClassC.java
package com.journaldev.inheritance; public class ClassC{ SuperClass obj = null; public ClassC(SuperClass o){ this.obj = o; } public void test(){ obj.doSomething(); } public static void main(String args[]){ ClassC obj1 = new ClassC(new ClassA()); ClassC obj2 = new ClassC(new ClassB()); obj1.test(); obj2.test(); } }
上面程式的輸出結果是:
doSomething implementation of A doSomething implementation of B
這種方法調用的靈活性在繼承中是不可用的,並且提升了支持組合優於繼承的最佳實踐。-
-
在組合中進行單元測試很容易,因為我們知道我們從超類中使用了哪些方法,我們可以為測試模擬它,而在繼承中我們嚴重依賴超類並不知道將會使用超類的哪些方法,所以我們需要測試超類的所有方法,這是額外的工作,並且因為繼承而不得不進行。
關於Java中的多重繼承以及對組合的簡要介紹就到這裡了。
Source:
https://www.digitalocean.com/community/tutorials/multiple-inheritance-in-java