Java | オブジェクト指向:default メソッド

Java Java
スポンサーリンク

default メソッドとは

default メソッドは「インターフェースに書く“簡易の既定実装”」です。Java 8 で導入され、インターフェースにメソッド本体を持たせられるようになりました。これにより、既存のインターフェースへ軽い機能追加をしても、全実装クラスを一斉修正せずに済みます。状態(フィールド)は持てない点は従来どおりで、あくまで“振る舞いの既定値”を提供するための仕組みです。


何が嬉しいのか(導入の背景と効果)

default メソッドの目的は「後方互換性の確保と軽量な共通ロジックの共有」です。ライブラリのインターフェースに新しいメソッドを追加しても、既存の実装は default のおかげでコンパイルが通ります。また、軽い前処理・後処理・ユーティリティ的な実装を契約側に置くことで、実装クラスの重複を減らせます。ただし骨格や状態管理のような重い設計は、抽象クラスに任せるのが定石です。


基本の書き方と最小例

既定の実装を持たせる

インターフェース内で default を使うと、中身のあるメソッドを書けます。実装クラスは必要に応じて上書きできます。

interface Normalizer {
    default String apply(String s) {               // 既定実装
        return s == null ? "" : s.trim();
    }
}

final class LowerNormalizer implements Normalizer {
    @Override
    public String apply(String s) {                // 差し替え(任意)
        var t = s == null ? "" : s.trim();
        return t.toLowerCase();
    }
}
Java

ユーティリティは static を使う

インターフェースに static メソッドも置けます。契約関連の補助関数はここにまとめると意図が明確です。

interface Normalizer {
    default String apply(String s) { return s == null ? "" : s.trim(); }
    static boolean empty(String s) { return s == null || s.isBlank(); }
}
Java

重要ポイントの深掘り(上書き・衝突・設計の線引き)

上書きの自由と契約の一貫性

default は“既定値”にすぎないため、実装側で自由に上書きできます。契約の意味(引数の前提、戻り値の定義、例外方針)はインターフェース側で明文化し、default もその契約を忠実に満たす実装にしておくと、上書き可否に関わらず一貫性が保たれます。

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

複数インターフェースが同名・同シグネチャの default を持つと衝突し、実装クラスで明示的な上書きが必須になります。A.super.m() のように、どれを採用するか選んだり合成したりして解決します。

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 は「軽い共通ロジック」向き、抽象クラスは「流れ(テンプレート)や不変条件を固定」する場が適切です。状態管理や初期化の順序を扱うなら抽象クラス、契約に沿った簡易な既定動作なら default と覚えておくと迷いません。


例題で体感する default の使いどころ

例 1: 既定の前処理を共有し、仕上げだけ差し替え

interface Formatter {
    default String preprocess(String s) {           // 共通の前処理
        return s == null ? "" : s.trim().replaceAll("\\s+", " ");
    }
    String format(String raw);                      // 契約(抽象)
}

final class UpperFormatter implements Formatter {
    @Override
    public String format(String raw) {
        String s = preprocess(raw);                 // default を再利用
        return s.toUpperCase();                     // 仕上げだけ差し替え
    }
}
Java

例 2: 後方互換のためのメソッド追加

interface Describable {
    String describe();                              // 既存契約
    default String json() {                         // 新規追加(既定)
        return "{\"desc\":\"" + describe() + "\"}";
    }
}

final class User implements Describable {
    @Override public String describe() { return "User"; } // 既存実装はそのままでOK
}
Java

既存の実装を壊さずに新機能(json)を提供できます。


つまずきやすい落とし穴と回避

default の過剰利用で“疑似基底クラス化”する

ロジックをインターフェースに盛りすぎると、依存が絡み合い読みにくくなります。重い共通処理や流れの固定は抽象クラスへ寄せ、default は軽い補助に留めるのが安全です。

状態を前提にした default を書かない

インターフェースは状態を持てません。外部状態に強く依存する default は設計のにおいです。必要なら契約を分割し、状態は実装クラス(または抽象クラス)に置きます。

衝突時の曖昧さ放置

複数の default が衝突したら、実装側で必ず上書いて明示解決します。どれを採用するか分からない状態は、後で意味論が崩れます。


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

default メソッドは「契約に沿った軽い既定実装」をインターフェースに持たせ、後方互換と重複削減を実現するための道具です。上書き自由を前提に、契約の意味を明確にし、衝突は実装側で必ず解決する。重い骨格は抽象クラスに任せ、default は前処理・補助・簡易な既定動作に限定——この線引きを守れば、拡張しやすく壊れにくい設計が手に入ります。

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