Java
Z
信頼度ランク
| S | 公式ソース確認済み |
| A | 成功実績多数・失敗例少数 |
| B | 賛否両論 |
| C | 動作未確認・セキュリティリスク高 |
| Z | 個人所感 |
Javaのインターフェースと抽象クラスの使い分け完全解説
インターフェースと抽象クラスの違い、default メソッド、sealed interface、いつどちらを使うべきかを実例で解説します。
一言結論
現代のJavaでは「何ができるか(能力)」の定義はインターフェース、「何者であるか(状態付きの共通基盤)」の定義は抽象クラスと使い分けるのが原則で、迷ったらインターフェースを選ぶべきだ。
基本的な違い
| 特徴 | インターフェース | 抽象クラス |
|---|---|---|
| 多重継承 | 複数実装可能 | 1クラスのみ継承 |
| フィールド | public static final のみ | 任意(インスタンス変数も可) |
| コンストラクタ | なし | あり |
| アクセス修飾子 | public のみ(Java 9以降は private も可) | 任意 |
| 状態の保持 | できない | できる |
インターフェース
public interface Drawable {
void draw(); // 暗黙的に public abstract
default void drawWithBorder() {
System.out.println("=== Border ===");
draw();
System.out.println("==============");
}
static Drawable noOp() {
return () -> {}; // ファクトリメソッド
}
}
public interface Resizable {
void resize(double factor);
}
// 複数のインターフェースを実装できる
public class Circle implements Drawable, Resizable {
private double radius;
@Override
public void draw() {
System.out.println("○ (radius=" + radius + ")");
}
@Override
public void resize(double factor) {
this.radius *= factor;
}
}
抽象クラス
public abstract class Animal {
// インスタンス変数(状態)を持てる
protected String name;
protected int age;
// コンストラクタを持てる
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
// 抽象メソッド(サブクラスで必ず実装)
public abstract String sound();
// 具体的な実装(共通処理)
public void introduce() {
System.out.println("私は" + name + "です。" + sound() + "と鳴きます。");
}
}
public class Dog extends Animal {
public Dog(String name, int age) {
super(name, age);
}
@Override
public String sound() {
return "ワン";
}
}
Java 8以降の default メソッド
インターフェースに具体的な実装を持つメソッドを追加できます。
public interface Logger {
void log(String message);
default void logWithTimestamp(String message) {
log(LocalDateTime.now() + " - " + message);
}
default void logError(String message, Throwable e) {
log("[ERROR] " + message + ": " + e.getMessage());
}
}
使いどころ: 既存のインターフェースに後方互換性を保ちながらメソッドを追加する場合。
Java 9以降の private メソッド
インターフェース内でのコードの共通化に使います。
public interface Validator<T> {
boolean isValid(T value);
default boolean isInvalidAndLog(T value) {
if (!isValid(value)) {
logValidationFailure(value);
return true;
}
return false;
}
private void logValidationFailure(T value) {
System.err.println("Validation failed for: " + value);
}
}
Java 17の sealed interface
実装クラスを制限できます。
public sealed interface Shape
permits Circle, Rectangle, Triangle {
double area();
}
public final class Circle implements Shape {
private final double radius;
@Override
public double area() {
return Math.PI * radius * radius;
}
}
permits で指定したクラス以外は実装できません。switch 式でのパターンマッチングと組み合わせると非常に強力です。
double area = switch (shape) {
case Circle c -> Math.PI * c.getRadius() * c.getRadius();
case Rectangle r -> r.getWidth() * r.getHeight();
case Triangle t -> 0.5 * t.getBase() * t.getHeight();
};
どちらを使うべきか
インターフェースを選ぶ場合
- 「できること(能力)」を定義する(
Comparable,Serializable,Runnable) - 複数の型の共通操作を定義する
- 実装者に実装の自由を与えたい
- 状態(フィールド)が必要ない
// 「何ができるか」を表現
interface Exportable {
byte[] toBytes();
}
interface Importable {
void fromBytes(byte[] data);
}
// 複数の能力を組み合わせられる
class Config implements Exportable, Importable { ... }
抽象クラスを選ぶ場合
- 「何であるか(is-a 関係)」を定義する
- 状態(インスタンス変数)を共有したい
- コンストラクタで初期化が必要
- サブクラスに共通の実装を提供したい(Template Method パターン)
// Template Method パターン
public abstract class DataProcessor {
// テンプレートメソッド(処理の骨格を定義)
public final void process() {
readData();
processData(); // サブクラスで実装
writeResults();
}
protected abstract void processData();
private void readData() { /* 共通処理 */ }
private void writeResults() { /* 共通処理 */ }
}
まとめ
インターフェース: 契約(何ができるか)を定義する。多重実装可能で柔軟。
抽象クラス: 共通の実装と状態を持つ基底クラス。継承ヒエラルキーを構築する際に使う。
現代の Java 開発では、default メソッドの登場によりインターフェースの表現力が増しました。「まずインターフェースを検討し、共有状態や複雑な初期化が必要なら抽象クラス」という順序で考えると良いでしょう。