Java組合與繼承

2019-10-16 22:23:25

在本小節中,我們將學習java多重繼承,比較組合和繼承。

java中的多重繼承是建立具有多個超類的單個類的功能。與其他一些流行的物件導向程式設計語言(如C++)不同,java不支援類中的多重繼承。

Java不支援一個類中的多個繼承,因為它可能產生一些問題(鑽石問題),Java有更好的方法可以實現與多重繼承相同的結果。

1. Java中的鑽石問題

為了方便理解鑽石問題,首先假設java中支援多個繼承。在這種情況下,可以有一個類層次結構,如下圖所示。

假設SuperClass是一個抽象類,宣告了一些方法。而ClassAClassB是繼承了SuperClass類的具體類。

檔案:SuperClass.java

public abstract class SuperClass {

    public abstract void doSomething();
}

檔案:ClassA.java

public class ClassA extends SuperClass{

    @Override
    public void doSomething(){
        System.out.println("doSomething implementation of A");
    }

    //ClassA 自己的方法
    public void methodA(){

    }
}

檔案:ClassB.java

public class ClassB extends SuperClass{

    @Override
    public void doSomething(){
        System.out.println("doSomething implementation of B");
    }

    //ClassB 自己的方法
    public void methodB(){

    }
}

現在假設ClassC實現類似於下面的內容,它同時擴充套件了ClassAClassB,這裡範例多重繼承。

檔案:ClassC.java

//  這只是為了解釋鑽石問題的假設
// 這段程式碼不能編譯通過
public class ClassC extends ClassA, ClassB{

    public void test(){
        //呼叫父類別的方法
        doSomething();
    }

}

請注意,test()方法中呼叫超類doSomething()方法。這就產生了歧義,因為編譯器不知道要執行哪個超類方法。由於鑽石類圖,在java中稱為鑽石問題。Java中的鑽石問題是java不支援類中多重繼承的主要原因。

2. Java介面多重繼承

前面我們說過類中不支援多繼承,但是使用介面可以實現。單個介面可以擴充套件多個介面,下面是一個簡單的例子。

檔案:InterfaceA.java

public interface InterfaceA {

    public void doSomething();
}

檔案:InterfaceB.java

public interface InterfaceB {

    public void doSomething();
}

請注意,兩個介面都宣告了相同的方法,現在可以使用一個介面來擴充套件這兩個介面,如下所示。

檔案:InterfaceC.java

public interface InterfaceC extends InterfaceA, InterfaceB {

    //same method is declared in InterfaceA and InterfaceB both
    public void doSomething();

}

這非常好,因為介面只宣告方法,實際的實現將由實現介面的具體類完成。因此,Java介面中的多重繼承中不存在任何歧義。

這就是java類可以實現多個介面的原因,如下例所示。

檔案:InterfacesImpl.java

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注釋。@Override注釋是三個內建java註釋之一,我們應該在覆蓋任何方法時始終使用@Override注釋。

2. Java組合

那麼如果想在ClassC中使用ClassA函式methodA()ClassB函式methodB()方法,該怎麼辦? 可使用組合方案解決。下面是ClassC的重構版本,它使用組合來使用兩個類中方法,並使用其中一個物件的doSomething()方法。

檔案:ClassC.java

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();
    }
}

3. 組合與繼承

Java程式設計的最佳實踐之一是「贊成組合而不是繼承」。下面將研究一些有利於這種方法的應用。

  1. 假設有一個超類和子類如下:
    檔案:ClassC.java
public class ClassC{

    public void methodC(){
    }
}

檔案:ClassD.java

public class ClassD extends ClassC{

    public int test(){
        return 0;
    }
}

上面的程式碼編譯並且工作正常,但是如果ClassC實現如下改變了怎麼辦:

檔案:ClassC.java

public class ClassC{

    public void methodC(){
    }

    public void test(){
    }
}

請注意,test()方法已存在於子類中,但返回型別不同。現在ClassD將無法編譯,如果使用任何IDE,它將建議您更改超類或子類中的返回型別。

現在想象一下,有多級類繼承和超類的情況不受控制。別無選擇,只能更改子類方法簽名或其名稱以刪除編譯錯誤。此外,將不得不在呼叫子類方法的所有地方進行更改,因此繼承會使程式碼變得脆弱。

上述問題永遠不會出現在組合中,這使得它比繼承更有利。

  1. 繼承的另一個問題是將所有超類方法暴露給用戶端,如果超類沒有正確設計並且存在安全漏洞,那麼即使完全注意實現類,也會受到糟糕實現的影響。
    組合有助於提供對超類方法的受控存取,而繼承不提供對超類方法的任何控制,這也是組合優於繼承的主要優點之一。

  2. 組合的另一個好處是它提供了呼叫方法的靈活性。上面的ClassC實現並不是最優的,它提供了與將被呼叫的方法的編譯時繫結,只需極少的更改,就可以使方法呼叫靈活並使其動態化。

檔案:ClassC.java

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

方法呼叫的這種靈活性在繼承中不可用,並且提升了最佳實踐以支援組合而不是繼承。

  1. 單元測試中很容易組合,因為我們知道在超類中使用的所有方法,可以模擬它進行測試,而在繼承中,很大程度上依賴於超類而不知道所有超類的方法將要使用,所以需要測試超類的所有方法。