4-20:抽象クラスよりもインターフェースを選ぶ
要点
新しく型の抽象化を作るときは、まずインターフェースを使う。抽象クラスは「実装の共有」が必要なときにのみ使う。
インターフェースは API(型)を柔軟に定義でき、実装側の自由度が高く、複数継承(型として)は可能にするため、一般に優先されます。
なぜインターフェースを優先するのか
-
多重型継承が可能:
クラスは implements で複数のインターフェースを実装できる一方、extends はクラスの単一継承しかできないため、インターフェースのほうが柔軟。 -
実装から契約を分離できる:
インターフェースは「何をするか(what)」を定義し、実際の実装(how)は各実装クラスに任せられます。 -
API の後方互換性が作りやすい:
Java 8 以降は default メソッドを使って既存インターフェースに機能を追加しやすく、既存実装を壊しにくい(ただし衝突や設計上の注意点はあり)。 -
実装選択の自由:
クライアントは任意の継承ツリーの中でインターフェースを実装でき、特定の抽象クラスに縛られない。 -
テストやモックが作りやすい:
インターフェースは実装依存がないため、Mockito 等によるモックやスタブの作成が簡単。
悪い例
// 悪い:API と実装を結びつけてしまう
public abstract class Shape {
protected int x, y;
public abstract double area();
public void moveTo(int nx, int ny) { this.x = nx; this.y = ny; }
}
問題点:
- Shape を継承しなければ area() を持てない(つまり同じ振る舞いを持ちたい他のクラスが使えない)。
- 実装共有(moveTo)のためだけに継承を強制してしまい、柔軟さを失う。
良い例
public interface Shape {
double area();
void moveTo(int x, int y);
}
- 任意のクラスが implements Shape して area() を実装できる。
- 実装を共有したければ別にヘルパークラス(ユーティリティ)やデフォルトメソッド/委譲を使う。
例:共通実装が必要なら
public interface Shape {
double area();
void moveTo(int x, int y);
default void moveBy(int dx, int dy) { // Java8+ の default
// デフォルト実装を置ける(互換性に配慮)
throw new UnsupportedOperationException();
}
}
// 実装共有はヘルパークラスで
public final class ShapeUtils {
public static void moveTo(Shape s, int x, int y) { ... }
}
抽象クラスを選ぶべき典型ケース
- 実装共有が本質的に重要で、サブクラスに同じ非trivialなコードを再利用させたいとき。
- 状態(インスタンスフィールド)を持たせたいとき:インターフェースはインスタンスフィールドを持てない。
- 新しいメソッドを追加するたびに実装の修正を要求したくない(ただし Java 8 の default があるが万能ではない)。
- たとえば「部分的に実装される抽象テンプレート」(テンプレートメソッドパターン)のときは抽象クラスが自然。
簡単な例(抽象クラスが適切):
public abstract class AbstractParser {
private final Buffer buffer; // 状態を持つ
protected AbstractParser(Buffer b) { this.buffer = b; }
public void parseAll() { // 共通処理
while (!buffer.empty()) parseNext();
}
protected abstract void parseNext();
}
インターフェースを使う際の注意点
-
default メソッドは乱用しない:
互換性維持のために便利だが、多重継承時に衝突(どの default を採るか)や設計のあいまいさが生じる。 -
インターフェースは状態(フィールド)を持てない:
必要なら別途ヘルパーか抽象クラスで実装共有する。 -
新メソッドの互換性:
Java 8 以前はインターフェースにメソッドを追加すると実装が壊れる。現代では default で補うが、意味的に正しいか考える。
まとめ
- まずインターフェース — 型を表すならインターフェースで設計するのが柔軟で将来性が高い。
- どうしても実装共有や状態が必要なときだけ抽象クラス を使う。
- Java 8 の default によりインターフェースはさらに強力になったが、設計意図を曖昧にしないよう乱用は避ける。