インターフェースとは
インターフェースは「クラスが満たすべき契約(メソッドの型だけ)を定義するもの」です。実装は持たず、まず“名前・引数・戻り値”を決めることで、呼び出し側は契約だけに依存できます。クラスは 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 + ")"; }
}
Javadefault と 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"); }
}
JavaApp は 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); }
}
JavaFormatter の実装を差し替えるだけで振る舞いを切り替えられます。
よくある落とし穴と回避(重要)
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 の乱用は避け、衝突時は実装側で上書きして意図を固定する——この型を守れば、疎結合でテストしやすく拡張に強い設計が自然にできます。
