信頼度ランク
| S | 公式ソース確認済み |
| A | 成功実績多数・失敗例少数 |
| B | 賛否両論 |
| C | 動作未確認・セキュリティリスク高 |
| Z | 個人所感 |
インターフェースと抽象クラスはなぜ両方あるのか — 「契約」と「未完成品」の違い
interfaceとabstract classの違いと使い分け。なぜ多重実装はできて多重継承はできないのか、デフォルトメソッドとは何か、ポリモーフィズムとの関係を根本から解説。
一言結論
インターフェースは「何ができるか」の契約、抽象クラスは「一部が未完成のクラス」。両者は目的が異なり、多重実装が可能な理由は実装を持たないことでDiamond問題を回避しているから。
「そういうもの」として覚えていないか
interface Flyable {
void fly(); // 実装なし
}
abstract class Vehicle {
abstract void move(); // 実装なし
void refuel() { ... } // 実装あり
}
「インターフェースはメソッドの中身を書けない、抽象クラスは書けるものもある」。これは正しいが、なぜ2つが存在するのか、どちらをいつ使うのかが分からないと応用できない。
インターフェースとは「契約」だ
インターフェースは「このクラスはこのメソッドを持つことを保証する」という契約だ。実装の中身はどうでもいい。「こういうことができる」という約束だけを定める。
interface Saveable {
void save(); // 「保存できる」という契約
}
Saveableをimplementsしたクラスは、必ずsave()を実装しなければならない。コンパイラがそれを強制する。
なぜこれが有用か?呼び出す側が実装の詳細を知らなくていいからだ:
// 呼び出す側は「Saveableである」ことだけ知っていれば動く
void saveAll(List<Saveable> items) {
for (Saveable item : items) {
item.save(); // FileでもDBでもクラウドでも、呼び方は同じ
}
}
class FileStorage implements Saveable {
public void save() { /* ファイルに保存 */ }
}
class DatabaseStorage implements Saveable {
public void save() { /* DBに保存 */ }
}
saveAll()はFileStorageでもDatabaseStorageでも動く。後からCloudStorageを追加してもsaveAll()を変更する必要がない。これが**ポリモーフィズム(多態性)**だ。
抽象クラスとは「未完成の設計図」だ
抽象クラスは「一部のメソッドが未実装のクラス」だ。共通の処理は親クラスに書き、クラスごとに違う部分だけを子クラスに任せる。
abstract class Report {
// 共通処理:全レポートに共通するヘッダー出力
void printHeader() {
System.out.println("=== レポート ===");
System.out.println("作成日: " + LocalDate.now());
}
// 抽象メソッド:各レポートが独自に実装する
abstract void printBody();
// テンプレートメソッドパターン:共通の流れを定義
void print() {
printHeader();
printBody(); // 子クラスによって異なる
}
}
class SalesReport extends Report {
@Override
void printBody() {
System.out.println("売上: 1000万円");
}
}
class InventoryReport extends Report {
@Override
void printBody() {
System.out.println("在庫: 500個");
}
}
ReportはprintHeader()という共通処理を持ちつつ、printBody()は各子クラスに実装を委ねている。コードの重複を避けつつ、違う部分だけをカスタマイズさせるのが抽象クラスの目的だ。
また、抽象クラスはnewできない:
Report r = new Report(); // ❌ エラー: Reportは抽象クラスなので直接インスタンス化できない
「未完成の設計図」から直接オブジェクトを作れないのは当然だ。
なぜ多重実装(implements)はできて多重継承(extends)はできないのか
Javaでは複数のインターフェースを同時に実装できる:
class Bird implements Flyable, Swimmable, Singable {
public void fly() { ... }
public void swim() { ... }
public void sing() { ... }
}
でも複数の親クラスからの継承はできない:
class C extends A, B { } // ❌ コンパイルエラー
なぜか?Diamond問題と呼ばれる曖昧さが生じるからだ:
A
/ \
B C
\ /
D ← DがBとCを継承したとしたら?
class A { void hello() { print("A"); } }
class B extends A { void hello() { print("B"); } }
class C extends A { void hello() { print("C"); } }
class D extends B, C { }
D d = new D();
d.hello(); // どちらのhello()を呼ぶ?BのものかCのものか?
BとCがhello()をオーバーライドしていた場合、Dはどちらを使えばいいかわからない。コンパイラが判断できない。
インターフェースが多重実装できる理由:インターフェースは(Java 8以前は)実装を持たないので、この問題が起きない。両方のhello()が実装なしならDが自分で実装するだけだ。
interface A { void hello(); } // 実装なし
interface B extends A { } // 実装なし
interface C extends A { } // 実装なし
class D implements B, C {
public void hello() { print("D"); } // Dが実装する。競合しない
}
Java 8以降のデフォルトメソッドと「本物のDiamond問題」
Java 8でインターフェースにデフォルトメソッド(実装を持てるメソッド)が追加された:
interface Greetable {
default void greet() {
System.out.println("こんにちは");
}
}
これで「インターフェースは実装を持たない」という前提が崩れた。でも多重実装でDiamond問題が起きたら?
Javaはルールで解決している:
interface A {
default void hello() { System.out.println("A"); }
}
interface B extends A {
default void hello() { System.out.println("B"); }
}
class C implements A, B {
// BがAをオーバーライドしているので、より具体的なBが優先される
// → Bのhello()が使われる
}
class D implements A, B {
@Override
public void hello() { B.super.hello(); } // 曖昧な場合は明示的に選ぶ
}
デフォルトメソッドは「既存のインターフェースに新しいメソッドを追加しても、実装クラスを全部修正しなくていい」という後方互換性のために設計された。Java 8でStreamやLambdaをListなどに追加するとき、既存の全実装クラスを修正せずに済んだのはこのデフォルトメソッドのおかげだ。
どちらを使うべきか
インターフェースを使う場面:
✅ 「できること」を定義したいとき(Comparable, Runnable, Serializable)
✅ 関係のないクラスに同じ操作を保証したいとき(BirdもAirplaneもFlyable)
✅ 実装の詳細に依存させたくないとき(テストのモック化も容易になる)
✅ 基本的にこちらをまず検討する
抽象クラスを使う場面:
✅ 共通の実装コードを子クラスで共有したいとき
✅ 「is-a」関係が明確なとき(SalesReportはReportである)
✅ コンストラクタやフィールドを持たせたいとき(インターフェースは持てない)
✅ protected メソッドを子クラスに提供したいとき
現代的なJavaでは「インターフェースをデフォルト、共通実装が必要なら抽象クラス」という方針が一般的だ。
まとめ
インターフェース = 「何ができるか」の契約。実装を持たない(デフォルトメソッドは例外)
抽象クラス = 「一部が未完成のクラス」。共通実装 + 子クラスへの委任
多重実装OK = インターフェースは(元々)実装を持たないのでDiamond問題が起きない
多重継承NG = 実装を持つ親クラスが複数あると、同名メソッドの衝突が解決できない
デフォルトメソッド = Java 8から。既存インターフェースへの追加を後方互換で可能にした
「実装があるかないか」だけで覚えると応用が利かない。「契約か未完成品か」という思想の違いを掴んでおくと、設計の判断が自然にできるようになる。