Java | オブジェクト指向:インターフェースとは

Java Java
スポンサーリンク

インターフェースとは

インターフェースは「クラスが満たすべき契約(メソッドの型だけ)を定義するもの」です。実装は持たず、まず“名前・引数・戻り値”を決めることで、呼び出し側は契約だけに依存できます。クラスは implements でインターフェースを複数採用でき、実体ごとに振る舞いを切り替える多態性が成立します。Java 8 以降は、例外的に簡単な既定実装(default メソッド)やユーティリティ(static メソッド)も持てますが、状態(フィールド)は持ちません。


何のために使うのか(重要)

インターフェースの目的は「依存を軽くし、差し替えを簡単にする」ことです。呼び出し側は具体クラスを知らずに契約だけで動けるため、テストではモック実装に置き換えやすく、本番では別実装へスムーズに差し替えられます。継承の強い結合を避け、役割ごとに疎結合な設計にできます。

interface PaymentGateway {
    boolean charge(int amount);
}

final class StripeGateway implements PaymentGateway {
    @Override public boolean charge(int amount) { System.out.println("stripe " + amount); return true; }
}

final class FakeGateway implements PaymentGateway { // テスト用
    @Override public boolean charge(int amount) { return amount >= 0; }
}

final class Service {
    private final PaymentGateway gw;
    Service(PaymentGateway gw) { this.gw = gw; }
    boolean buy(int amount) { return amount > 0 && gw.charge(amount); }
}
Java

契約(PaymentGateway)に依存することで、呼び出し側の Service は差し替えに強く、テストも楽になります。


基本ルールと構文

宣言と実装のポイント

  • 宣言: interface キーワードで定義し、メソッドは本体なし(抽象)で宣言します。
  • 実装: クラスは implements で採用し、すべてのメソッドを実装します(未実装ならそのクラスも abstract)。
  • 多重実装: クラスは複数のインターフェースを同時に実装できます(役割の合成に強い)。
interface Describable {
    String describe();
}

final class User implements Describable {
    private final String id;
    User(String id) { this.id = id; }
    @Override public String describe() { return "User(" + id + ")"; }
}
Java

default と static(Java 8+)

  • default メソッド: 簡易の既定実装を持てます。必要なら実装クラスで上書きできます。
  • static メソッド: 契約関連のユーティリティを置けます。状態は持てません。
interface Normalizer {
    default String apply(String s) { return s == null ? "" : s.trim(); } // 既定
    static boolean empty(String s) { return s == null || s.isBlank(); }  // ユーティリティ
}
final class LowerNormalizer implements Normalizer {
    @Override public String apply(String s) { return Normalizer.empty(s) ? "" : s.trim().toLowerCase(); }
}
Java

抽象クラスとの違いと使い分け(重要な深掘り)

  • 状態の有無: インターフェースは状態(フィールド)を持てません。抽象クラスは持てます。
  • 継承の数: クラスは1つの抽象/具象クラスしか継承できませんが、インターフェースは複数同時に実装可能です。
  • 骨格の固定: 処理の流れや不変条件を親で管理したいなら抽象クラス。役割の契約だけを共有したいならインターフェース。
  • 組み合わせ: 抽象クラスを1つ継承しつつ、複数インターフェースを実装するのが実務でよくある形。
interface Task { void run(); }

abstract class BaseJob implements Task {
    @Override public final void run() { setup(); execute(); teardown(); } // 流れ固定
    protected void setup() {}
    protected abstract void execute();
    protected void teardown() {}
}

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

役割(Task)はインターフェースで共有し、流れは抽象クラスで固定。この併用で柔軟さと安全性が両立します。


例題で理解する置換可能性

ロガーの差し替え

interface Logger {
    void info(String msg);
}

final class ConsoleLogger implements Logger {
    @Override public void info(String msg) { System.out.println("[INFO] " + msg); }
}

final class SilentLogger implements Logger { // テスト用
    @Override public void info(String msg) { /* no-op */ }
}

final class App {
    private final Logger logger;
    App(Logger logger) { this.logger = logger; }
    void run() { logger.info("start"); }
}
Java

App は Logger 契約だけに依存するため、環境ごとに簡単に差し替え可能です。

ストラテジ(戦略)の切替

interface Formatter { String format(String raw); }

final class UpperFormatter implements Formatter {
    @Override public String format(String raw) { return raw == null ? "" : raw.toUpperCase(); }
}
final class SnakeFormatter implements Formatter {
    @Override public String format(String raw) { return raw == null ? "" : raw.trim().replaceAll("\\s+", "_"); }
}

final class NameService {
    private final Formatter formatter;
    NameService(Formatter f) { this.formatter = f; }
    String format(String s) { return formatter.format(s); }
}
Java

Formatter の実装を差し替えるだけで振る舞いを切り替えられます。


よくある落とし穴と回避(重要)

default 多用で「疑似基底クラス化」する

インターフェースにロジックを盛りすぎると、依存が絡み合い読みにくくなります。default は軽い補助に留め、骨格は抽象クラスに任せます。

ダイヤモンド問題(default の衝突)

複数インターフェースが同名の default を持つと衝突します。実装側で明示的に上書きして解消します。

interface A { default String x() { return "A"; } }
interface B { default String x() { return "B"; } }
final class C implements A, B {
    @Override public String x() { return A.super.x() + "+" + B.super.x(); } // 明示解決
}
Java

契約の曖昧さ

引数の前提、戻り値の意味、例外方針をドキュメントやメソッド名で明確に。検証は呼び出し側か、抽象クラスの骨格で行いましょう。


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

インターフェースは「役割の契約を分離し、実装を自由に差し替える」ための軸です。状態は持たず、軽い default と static で補助しつつ、骨格や不変条件が必要なら抽象クラスと併用する。契約は明確に、default の乱用は避け、衝突時は実装側で上書きして意図を固定する——この型を守れば、疎結合でテストしやすく拡張に強い設計が自然にできます。

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