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がクラスで複数の継承をサポートしていない主な理由です。3つのクラスだけでなく、複数のクラス継承の上記の問題も同様に発生することに注意してください。

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アノテーションを使用しています。オーバーライドアノテーションは3つの組み込みのJavaアノテーションの1つであり、メソッドをオーバーライドする際には常にオーバーライドアノテーションを使用する必要があります

コンポジションで救済する

では、ClassCClassAmethodA()ClassBmethodB()を利用したい場合はどうすればよいでしょうか。その解決策は、コンポジションを使用することにあります。以下は、両方のクラスのメソッドを利用し、またオブジェクトの1つからdoSomething()メソッドを使用するためにコンポジションを使用してリファクタリングされたClassCのバージョンです。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つは、「継承よりも構成を好む」ということです。このアプローチを支持するいくつかの側面について調べてみましょう。

  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. コンポジションのもう1つの利点は、メソッドの呼び出しに柔軟性を提供することです。上記の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();
    	}
    }
    

    上記のプログラムの出力は次のとおりです:

    AのdoSomethingの実装
    BのdoSomethingの実装
    

    このようなメソッドの呼び出しの柔軟性は継承では利用できず、継承よりもコンポジションを好むベストプラクティスを促進します。

  4. コンポジションではユニットテストが容易です。スーパークラスから使用するメソッドがわかっているため、テスト用にそれをモックアップすることができます。一方、継承ではスーパークラスに大きく依存し、スーパークラスのすべてのメソッドが使用されるかわかりません。したがって、スーパークラスのすべてのメソッドをテストする必要があります。これは余分な作業であり、継承のために不必要に行う必要があります。

これにてJavaにおける多重継承とコンポジションの概要を説明しました。

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