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 は前処理・補助・簡易な既定動作に限定——この線引きを守れば、拡張しやすく壊れにくい設計が手に入ります。
