はじめに
最近、Java SE 17 Programmer I (1Z0-825-JPN) 試験の勉強をしていています。
予想問題で次のような観点が問われると、「え、どうだったっけ...?」と思考停止することがありました。
- 抽象クラスって多重継承できるの?
- クラスって多重継承できるの?
- インタフェースって多重継承できるの?
- 実装クラスで多重実装できるの?
これらの問題は一見バラバラに見えますが、実はある1つのルールを理解しておくことで、すべて整理できると気づきました。この記事では、自分なりに見つけたその本質を共有し、同じように悩む方の参考になればと思います。
いきなり結論
では、上記の問題に共通する“本質”とは何か?
私がたどり着いた考えは...
実装クラスから見て、同じメソッド名・引数(=シグネチャ)を持つメソッドが複数存在しないようにすること
です!
え?どういうこと?と思いますよね。
このあと順を追って説明していきます
クラスの多重継承は行えない
まず、Javaではクラスの多重継承(class A extends B, C
のような構文)はできません。これは公式ドキュメントでも明記されています。
Each class except Object is an extension of (that is, a subclass of) a single existing class and may implement interfaces
(Objectを除く各クラスは、単一の既存クラスの拡張(つまりサブクラス)であり、インターフェースを実装することができます。)
なぜNG?
いきなり結論の観点から考えると、もし、2つの親クラスそれぞれで同じメソッドシグネチャ(例:void hello()
)の実装があった場合:
-
hello()
の実装も2つになってしまい、どのhello()
を継承すればいいか曖昧になる
→ 「実装クラスから見たときに実装が複数存在してしまう」→NG! となります。
インタフェースの多重継承は行える
Javaではインタフェースの多重継承は可能です。複数のインタフェースをextends
で継承することができますし、実装クラスが複数のインタフェースをimplements
することも可能です。
interface A { void hello(); }
interface B { void goodbye(); }
// インタフェースCがインタフェースAとBを多重継承する
interface C extends A, B {}
// 実装クラスDがインタフェースCを実装する
class D implements C {
public void hello() {
System.out.println("Hello");
}
public void goodbye() {
System.out.println("Goodbye");
}
}
なぜOK?
これも“いきなり結論”の観点で説明できます。
- インタフェースはメソッドの宣言だけを行い、実装は持たない(Java 8以前の話)
- どのインタフェースに
hello()
があろうと、hello()
の実装自体は実装クラスで行われる
→ 「実装クラスから見たときにhello()
の実装は"単一"となる」→OK! となります。
defaultメソッドの登場
ところが、Java 8からは、インタフェースでもdefault
メソッドによって実装を持てるようになりました。
interface A { default void hello() { System.out.println("Hello from A"); } }
interface B { default void goodbye() { System.out.println("Goodbye from B"); } }
class C implements A, B {
}
“いきなり結論”との要点に反してない?
そう、ここで“いきなり結論”と矛盾するように見えます。
複数のインタフェースが実装付き(default)の同じメソッドを持っていると、
実装クラスがどちらの実装を使えばいいか判断できないです。
そのため、以下のようなソースコードは、コンパイルエラーとなります。
interface A { default void hello() { System.out.println("Hello from A"); } }
interface B { default void hello() { System.out.println("Hello from B"); } }
class C implements A, B {
// コンパイルエラー: AとB、どちらのhello()を使えばいいか分からない
}
解決方法としては、明示的にオーバーライドします。
こうすることによって、同じシグネチャの実装が単一となり、"いきなり結論”にも当てはめることができました。
class C implements A, B {
@Override
public void hello() {
A.super.hello(); // または B.super.hello()
}
}
余談:そもそもインタフェースの多重継承も制限すべきでは?
上記の通り、defaultメソッドの登場によって、インタフェース同士でもメソッドの衝突が起きるようになりました。
そのため「インタフェースの多重継承も禁止した方が良いのでは?」と私は感じました。
この疑問をAIにぶつけてみたところ、以下のような返答をもらいました:
defaultメソッドは後発で導入された機能であり、Javaの既存の柔軟性(インタフェースの多重継承)を壊さずに、実装の共有を可能にした設計です。
衝突の可能性がある場合は、開発者が明示的に解決することで安全性と柔軟性の両立を図っています。
なるほど、つまり、
- 既存バージョンとの互換性や、柔軟性(多重継承)は維持しつつ
- 衝突時は開発者に責任を持たせる設計
とすることでバランスが取られている、ということですね。
冒頭で挙げた問題の回答
まとめへ入る前に、冒頭で挙げた問題の答えをここで簡単に整理しておきます:
- 抽象クラスって多重継承できるの?
- できません。 クラスの多重継承自体が禁止されているため、抽象クラスでも同様です
- クラスって多重継承できるの?
- できません。 上記と同じ理由です
- インタフェースって多重継承できるの?
- できます。 複数のインタフェースをextendsで継承できます
- 実装クラスで多重実装できるの?
- できます。 複数のインタフェースをimplementsできます。ただし、defaultメソッドが衝突する場合は明示的なオーバーライドが必要です
おわりに
Javaの継承仕様を整理すると、「実装クラスから見て、同じメソッド名・引数(=シグネチャ)を持つメソッドが複数存在しないようにする」という原則が一貫している、というのが私なりの理解です。
defaultメソッドを考慮すると、一見この原則に反するようにも思えますが、これはJavaの互換性を維持するための設計です。明示的にオーバーライドすることで、結果としてこの原則にも合致するようになっています。
これを意識すれば、抽象クラス・インタフェース・defaultメソッドの扱いにも納得がいきやすくなります。
資格試験のための知識も、こうした「ルールの背景」を理解しておけば、暗記に頼らず理解で乗り切れるはずです。この記事が少しでもその助けになれば幸いです!