Java | オブジェクト指向:インターフェースの多重実装

Java Java
スポンサーリンク

インターフェースの多重実装とは

インターフェースの多重実装は「1つのクラスが複数のインターフェース契約を同時に満たす」ことです。Javaはクラスの多重継承はできませんが、インターフェースは複数まとめて implements できます。これにより、1つのクラスに複数の“役割”を与え、呼び出し側は必要な契約だけを見る疎結合な設計が実現します。


何が嬉しいのか(重要)

複数の役割を合成できるため、機能を柔らかく組み合わせられます。たとえば「ログ出力できる」「実行できる」「閉じられる」などの契約を分離し、クラスは必要なものだけ採用します。呼び出し側は「Logger として」「Runnable として」と文脈ごとに扱えるため、差し替え・テスト・拡張が簡単になります。大規模でも“契約ごとに独立”するので、依存が絡みにくく保守が楽です。


基本構文と最小例

複数インターフェースの同時 implements

インターフェースをカンマ区切りで並べ、すべてのメソッドを実装します。

interface Logger { void info(String msg); }
interface Runner { void run(); }

final class App implements Logger, Runner {
    @Override public void info(String msg) { System.out.println("[INFO] " + msg); }
    @Override public void run() { info("start"); }
}
Java

App は Logger と Runner の両方として扱えます。文脈に応じて必要な契約だけ渡すと依存が軽くなります。


例題で理解する役割合成

例 1: 監査可能なタスク

interface Task { void execute(); }
interface Auditable { void audit(String tag); }

final class EmailTask implements Task, Auditable {
    @Override public void execute() { System.out.println("send email"); }
    @Override public void audit(String tag) { System.out.println("[AUDIT] " + tag); }
}

// 呼び出し側は必要な役割だけに依存
void runTask(Task t) { t.execute(); }
void logAudit(Auditable a) { a.audit("start"); }
Java

同じインスタンスを Task としても Auditable としても使えるため、配線が柔らかく保てます。

例 2: 正規化と検証の分離

interface Normalizer { String apply(String s); }
interface Validator { boolean valid(String s); }

final class UserNameRules implements Normalizer, Validator {
    @Override public String apply(String s) {
        return s == null ? "" : s.trim().replaceAll("\\s+", " ");
    }
    @Override public boolean valid(String s) {
        var t = apply(s);
        return !t.isBlank() && t.length() <= 50;
    }
}
Java

同一のルールセットを「整形器」と「検証器」として使い分け可能。テストでは片方の契約だけモックにすることもできます。


default メソッドの衝突と解決(重要な落とし穴)

複数インターフェースが同名・同シグネチャの default メソッドを持つと衝突します。実装側で「どれを採用するか」または「独自の上書き」を必ず明示してください。

interface A { default String tag() { return "A"; } }
interface B { default String tag() { return "B"; } }

final class C implements A, B {
    @Override
    public String tag() {
        return A.super.tag() + "+" + B.super.tag(); // 明示的に両方を合成
    }
}
Java

解決のコツは「衝突させない命名」か「実装側で一本化」。重い共通処理は抽象クラスに寄せ、default は軽い補助に留めると衝突が減ります。


抽象クラスとの併用で骨格を固定する

インターフェースは“役割の契約”を与え、抽象クラスは“流れ・不変条件・共通ロジック”を与えます。多重実装で役割を合成しつつ、抽象クラスで安全な骨格を固定すると、柔軟性と安全性のバランスが取れます。

interface Task { void run(); }
interface Logger { void info(String msg); }

abstract class BaseTask implements Task, Logger {
    @Override public final void run() {
        info("begin");
        try { execute(); } finally { info("end"); }
    }
    protected abstract void execute();
    @Override public void info(String msg) { System.out.println("[INFO] " + msg); } // 既定実装
}

final class EmailTask extends BaseTask {
    @Override protected void execute() { System.out.println("email"); }
}
Java

呼び出し側は Task だけに依存、ログの扱いは骨格で統一。役割合成+骨格固定の定石です。


設計指針(重要な深掘り)

役割は小さく、明確に切る

1つのインターフェースは“単一責務”に絞り、名前・引数・戻り値の意味を明確にします。こうすると多重実装で合成しやすく、テストも容易です。

default は軽く、骨格は抽象クラスへ

複数の default が絡み始めたら、抽象クラスに共通処理を移して衝突を避けます。インターフェースは契約中心に。

注入で依存を緩める

多重実装したからといって常に“1インスタンスで全部”を持つ必要はありません。コンストラクタでそれぞれの役割を別インスタンスとして注入し、合成クラスは委譲でまとめても構いません(アダプタ・デコレータの発想)。


つまずきポイントと回避

契約の曖昧さ

「何を保証するメソッドか」をドキュメントや命名で固定します。前提(null許容、範囲)と例外方針を明記するだけで、差し替えの安全性が上がります。

役割が巨大化する

1インターフェースに多機能を詰め込むと、多重実装の利点が薄れます。分割(インターフェース分離の原則)で小さく保つのが鉄則です。

default のダイヤモンド問題

衝突は実装側で必ず上書き。曖昧さを残すと意味論が崩れます。必要なら A.super.m() のように明示解決します。


仕上げのアドバイス(重要部分のまとめ)

インターフェースの多重実装は「役割を小さく分け、必要なだけ合成して差し替えに強くする」ための武器です。契約は明確に、default は軽く、骨格は抽象クラスに寄せる。衝突は実装側で明示解決し、依存は注入で緩める——この型を守れば、拡張しやすくテストしやすい疎結合設計が自然に手に入ります。

タイトルとURLをコピーしました