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); } // 実体に応じて切り替わる
Javadefault と 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();
}
}
Javadefault の衝突(ダイヤモンド問題)の解決
複数インターフェースが同名の 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 を徹底する——この型を守れば、疎結合でテストしやすく拡張に強いコードになります。

