Java | オブジェクト指向:implements

Java Java
スポンサーリンク

implements とは何か

implements は「クラスがインターフェースの契約を実装します」という宣言です。インターフェースが定めるメソッドの“名前・引数・戻り値”を、クラス側で実体として提供します。これにより、呼び出し側は具体クラスに依存せず、契約(インターフェース)だけに依存できるため、差し替えが簡単で疎結合な設計が実現します。


基本構文と最小例

1つのインターフェースを実装する

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

複数のインターフェースを同時に実装する

Java はクラスの多重継承は不可ですが、インターフェースの多重実装は可能です。複数の役割を合成し、柔軟な設計にできます。

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

なぜ implements を使うのか(重要ポイントの深掘り)

依存を軽くして差し替え可能にする

呼び出し側は契約(インターフェース)だけ知れば動くため、実装の変更やテスト用の差し替えが容易になります。大規模コードで保守コストを劇的に下げる効果があります。

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

ポリモーフィズムで分岐を消す

インターフェース型に統一すれば、実体に応じて自動で振る舞いが切り替わります。呼び出し側に if/else の分岐を増やさずに拡張できます。

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+", "_"); }
}

String render(Formatter f, String s) { return f.format(s); } // 実体に応じて切り替わる
Java

default と static をどう扱うか

default メソッドの既定実装

インターフェースは軽い既定実装を持てます。implements したクラスは必要に応じて上書き可能です。骨格や重いロジックは抽象クラスへ、軽い補助は default へが目安です。

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

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

抽象クラスとの併用と設計の型(重要)

役割はインターフェース、流れと不変条件は抽象クラス

骨格(前処理・検証・後処理)は抽象クラスで固定し、役割の契約はインターフェースで共有するのが実務の定石です。柔軟性と安全性が両立します。

interface Task { void run(); }

abstract class BaseTask implements Task {
    @Override
    public final void run() {
        setup();
        try { execute(); } finally { teardown(); }
    }
    protected void setup() {}
    protected abstract void execute();
    protected void teardown() {}
}

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

例題で体感する implements の威力

ロガーの注入と差し替え

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 for tests */ }
}

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

本番では ConsoleLogger、テストでは SilentLogger を渡すだけで振る舞いが切り替え可能です。

ストレージの入れ替え

interface Storage { void put(String key, String value); }

final class MemoryStorage implements Storage {
    private final java.util.Map<String, String> map = new java.util.HashMap<>();
    @Override public void put(String key, String value) { map.put(key, value); }
}

final class FileStorage implements Storage {
    @Override public void put(String key, String value) { System.out.println("write " + key + "=" + value); }
}

void saveAll(Storage st) {
    st.put("a", "1");
    st.put("b", "2");
}
Java

呼び出し側は Storage 契約だけに依存し、実体は自由に差し替えられます。


つまずきやすいポイントと回避(重要な深掘り)

契約の曖昧さを残さない

インターフェースのメソッドは、引数の前提、戻り値の意味、例外方針を明確にします。名前とドキュメントで意図を固定し、必要な検証は抽象クラスの骨格や呼び出し側で行います。

default の過剰利用を避ける

default にロジックを盛りすぎると“疑似基底クラス化”して読みにくくなります。重い共通処理は抽象クラスへ、インターフェースは契約を中心に。

実装漏れは @Override で防ぐ

implements したら、各メソッドに @Override を付ける習慣を徹底します。シグネチャのズレや意図しないオーバーロードをコンパイル時に潰せます。


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

implements は「役割の契約を採用し、差し替え可能な設計にする」ための要です。複数インターフェースで役割を合成しつつ、骨格や不変条件は抽象クラスで固定する。default は軽い補助に留め、衝突は実装側で明示解決。契約の意味をはっきりさせ、@Override を徹底する——この型を守れば、疎結合でテストしやすく拡張に強いコードになります。

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