Javaにおける多重継承 (Java no okeku ta jūkei)
今日はJavaの多重継承について調べてみましょう。少し前に、Javaでの継承、インターフェース、およびコンポジションについていくつかの投稿を書きました。この投稿では、Javaの多重継承を見てから、コンポジションと継承を比較してみましょう。
Javaにおける多重継承
Javaでは、複数のスーパークラスを持つ単一のクラスを作成することができるのがマルチプル継承です。C++など他の人気のあるオブジェクト指向プログラミング言語とは異なり、Javaではクラスにおいてマルチプル継承をサポートしていません。Javaでは、ダイヤモンド問題を引き起こす可能性があるため、マルチプル継承をサポートしていません。その複雑な問題を解決するための方法を提供する代わりに、同じ結果を得るためのより良い方法があります。
Javaにおけるダイヤモンドの問題
ダイヤモンドの問題を簡単に理解するために、Javaで多重継承がサポートされていると仮定してみましょう。その場合、以下のイメージのようなクラス階層が存在するでしょう。スーパークラスは抽象クラスであり、いくつかのメソッドを宣言しています。クラスAとクラスBは具象クラスです。SuperClass.javaというファイルです。
package com.scdev.inheritance;
public abstract class SuperClass {
public abstract void doSomething();
}
クラスA.java
package com.scdev.inheritance;
public class ClassA extends SuperClass{
@Override
public void doSomething(){
System.out.println("doSomething implementation of A");
}
//ClassA own method
public void methodA(){
}
}
クラスB.java
package com.scdev.inheritance;
public class ClassB extends SuperClass{
@Override
public void doSomething(){
System.out.println("doSomething implementation of B");
}
//ClassB specific method
public void methodB(){
}
}
今、ClassCの実装は以下のようなものであり、ClassAとClassBの両方を拡張しています。ClassC.javaという名前です。
package com.scdev.inheritance;
// this is just an assumption to explain the diamond problem
//this code won't compile
public class ClassC extends ClassA, ClassB{
public void test(){
//calling super class method
doSomething();
}
}
test()メソッドがスーパークラスのdoSomething()メソッドを呼び出していることに気づきます。これにより、コンパイラはどのスーパークラスのメソッドを実行するかわからなくなります。ダイヤモンド形のクラス図のため、これはJavaにおけるダイヤモンド問題と呼ばれています。Javaにおけるダイヤモンド問題は、クラスにおける多重継承をサポートしていない主な理由です。上記の多重クラスの継承に関する問題は、少なくとも1つの共通メソッドを持つ3つのクラスでも発生することに注意してください。
Javaインタフェースにおける多重継承
クラスでは多重継承がサポートされていないということに気づいたかもしれませんが、インターフェースではサポートされています。単一のインターフェースは複数のインターフェースを拡張することができます。以下は簡単な例です。InterfaceA.java
package com.scdev.inheritance;
public interface InterfaceA {
public void doSomething();
}
InterfaceB.javaの内容を日本語でパラフレーズすると、以下の通りです :
InterfaceB.javaは、インターフェースBのJavaファイルです。
package com.scdev.inheritance;
public interface InterfaceB {
public void doSomething();
}
以下のように、両方のインターフェースが同じメソッドを宣言していることに注意してください。今、以下のようにこれらのインターフェースの両方を拡張するインターフェースを持つことができます。InterfaceC.java
package com.scdev.inheritance;
public interface InterfaceC extends InterfaceA, InterfaceB {
//same method is declared in InterfaceA and InterfaceB both
public void doSomething();
}
以下は、ネイティブな日本語での一つのオプションです:
これは完璧に問題ありません。インタフェースはメソッドの宣言のみを行い、実際の実装はインタフェースを実装する具体的なクラスによって行われます。そのため、Javaのインタフェースでは複数の継承においてあらゆる曖昧さの可能性がないのです。それゆえ、Javaのクラスは複数のインタフェースを実装することができます。以下の例のようなものです。InterfacesImpl.java
package com.scdev.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();
//all the method calls below are going to same concrete implementation
objA.doSomething();
objB.doSomething();
objC.doSomething();
}
}
私がスーパークラスのメソッドをオーバーライドしたり、インターフェースのメソッドを実装する際に、毎回@Overrideアノテーションを使用していることに気づきましたか。Overrideアノテーションは、3つの組み込みJavaアノテーションのうちの1つであり、メソッドをオーバーライドする際には常にOverrideアノテーションを使用すべきです。
救助のための作曲 (Kyūjo no tame no sakkyoku)
クラスCでClassAのfunction methodA()とClassBのfunction methodB()を利用したい場合、コンポジションを使用することで解決策が見つかります。以下は、両クラスのメソッドを利用し、さらにオブジェクトの中からdoSomething()メソッドを使用するためにコンポジションを利用したリファクタリングされたClassCのバージョンです。ClassC.java
package com.scdev.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();
}
}
コンポジションと継承の比較
Javaプログラミングのベストプラクティスの一つは「継承よりもコンポジションを優先する」ということです。このアプローチを優先する要素のいくつかについて見ていきましょう。
-
- 次のように、スーパークラスとサブクラスがあると仮定しましょう。ClassC.java
-
- package com.scdev.inheritance;
public class ClassC{
public void methodC(){
}
}
ClassD.java
package com.scdev.inheritance;
public class ClassD extends ClassC{
public int test(){
return 0;
}
}
上記のコードはコンパイルして正常に動作しますが、ClassCの実装が以下のように変更された場合にはどうなるでしょうか。ClassC.java
package com.scdev.inheritance;
public class ClassC{
public void methodC(){
}
public void test(){
}
}
注意点としては、test()メソッドはすでにサブクラスに存在していますが、戻り値の型が異なります。このため、ClassDはコンパイルできなくなり、IDEを使用している場合はスーパークラスまたはサブクラスの戻り値の型を変更するよう提案されるでしょう。それでは、スーパークラスが制御できない多段階のクラス継承という状況を想像してみてください。私たちはコンパイルエラーを解消するために、サブクラスのメソッドのシグネチャや名前を変更せざるを得ない状況になります。また、サブクラスのメソッドが呼び出されるすべての場所で変更を加える必要がありますので、継承は私たちのコードを壊れやすくします。これに対して、コンポジションではこのような問題は発生せず、それが継承よりも好ましいとされる理由です。
継承の別の問題は、すべてのスーパークラスのメソッドをクライアントに公開していることです。もしスーパークラスが適切に設計されておらず、セキュリティホールがある場合、私たちがクラスを完全に実装に気をつけていても、スーパークラスの不正な実装の影響を受けてしまいます。コンポジションはスーパークラスのメソッドに制御されたアクセスを提供するのに対し、継承はスーパークラスのメソッドに制御を持たないため、これもコンポジションが継承に対して優れている主な利点の一つです。
コンポジションのもう一つの利点は、メソッドの呼び出しにおいて柔軟性を提供することです。上記のClassCの実装は最適ではなく、実行時に呼び出されるメソッドにコンパイル時のバインディングを提供しています。最小限の変更でメソッドの呼び出しを柔軟にし、動的にすることができます。ClassC.java
package com.scdev.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の実装
このようなメソッド呼び出しの柔軟性は継承では利用できず、コンポジションを好むためのベストプラクティスを促進します。
コンポジションでは、ユニットテストが簡単です。スーパークラスから使用しているすべてのメソッドを把握し、テストのためにそれをモックアップすることができますが、継承ではスーパークラスに大きく依存しており、スーパークラスのすべてのメソッドが使用されるかどうかわからないため、スーパークラスのすべてのメソッドをテストする必要があります。これは余分な作業であり、継承のために不必要な作業をする必要があります。
それがすべてJavaにおける多重継承とコンポジションについての簡単な説明でした。