Java 中的多重繼承

今天我們將探討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 編程的最佳實踐之一是「優先選擇組合而非繼承」。我們將探討一些支持這種方法的方面。

  1. 假設我們有一個超級類和子類如下: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,它將建議您更改超類或子類中的返回類型。現在想像一下,如果我們有多層級的類繼承,而超類不是我們控制的。我們將不得不改變子類的方法簽名或其名稱以消除編譯錯誤。此外,我們還必須更改所有調用子類方法的地方,因此繼承使我們的代碼變得脆弱。這個問題在組合中永遠不會發生,這使得組合比繼承更受青睞。

  2. 繼承的另一個問題是我們向客戶端公開了所有的超類方法,如果我們的超類設計不良並存在安全漏洞,則即使我們在實現自己的類時完全小心,也會受到超類不良實現的影響。組合幫助我們對超類方法提供受控訪問,而繼承不提供任何對超類方法的控制,這也是組合優於繼承的主要優勢之一。

  3. 組合的另一個好處是它提供了方法調用的靈活性。我們上面的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
    

    這種方法調用的靈活性在繼承中是不可用的,並且提升了支持組合優於繼承的最佳實踐。-

  4. 在組合中進行單元測試很容易,因為我們知道我們從超類中使用了哪些方法,我們可以為測試模擬它,而在繼承中我們嚴重依賴超類並不知道將會使用超類的哪些方法,所以我們需要測試超類的所有方法,這是額外的工作,並且因為繼承而不得不進行。

關於Java中的多重繼承以及對組合的簡要介紹就到這裡了。

Source:
https://www.digitalocean.com/community/tutorials/multiple-inheritance-in-java