【Java】Javaのinterfaceデフォルトメソッド・静的メソッドを使いこなす

Java

こんにちは、かつコーチです。
今回はJava 8以降のinterfaceに追加された、デフォルトメソッドと静的メソッドを扱います。
「interfaceは抽象メソッドしか持てない」という前提知識をアップデートしたい方向けの内容です。

デフォルトメソッドが解決した後方互換性の問題

なぜJava 8でinterfaceが変わったのか

Java 8以前、interfaceは抽象メソッドの宣言しか持てませんでした。
そのため既存のinterfaceに新しいメソッドを1つ追加するだけで、それを実装している全クラスがコンパイルエラーになっていました。

Java 8でStream APIをCollection系のinterfaceに追加する際、この制約が問題になりました。
既存の実装クラスをすべて修正するのは現実的ではなかったからです。
そこで導入されたのがdefaultメソッドです。
実装本体をinterface側に持たせることで、既存の実装クラスを変更せずにメソッドを追加できるようになりました。

デフォルトメソッドの基本構文

public interface Greeter {
    String getName();

    default String greet() {
        return "こんにちは、" + getName() + "さん";
    }
}

public class JapaneseGreeter implements Greeter {
    private final String name;

    public JapaneseGreeter(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return name;
    }
}

JapaneseGreetergreet()を一切実装していませんが、interfaceのデフォルト実装がそのまま使えます。

Greeter greeter = new JapaneseGreeter("田中");
System.out.println(greeter.greet()); // こんにちは、田中さん

静的メソッドとファクトリメソッドの活用

interfaceに静的メソッドを持たせる

staticメソッドもJava 8からinterfaceに定義できます。
実装クラスに継承されないため、ユーティリティメソッドやファクトリメソッドの置き場として便利です。

public interface Discount {
    double apply(double price);

    static Discount none() {
        return price -> price;
    }

    static Discount percentage(double rate) {
        return price -> price * (1 - rate);
    }
}

呼び出し側はDiscount.percentage(0.1)のようにクラス名感覚で呼べます。
Comparator.naturalOrder()List.of()も同じ仕組みで、実装の詳細を隠しつつ生成方法を提供しています。

実装で詰まったポイント:多重継承の衝突

複数のinterfaceを実装し、同名のデフォルトメソッドが衝突したときにコンパイルエラーになったことがあります。

public interface A {
    default String label() {
        return "A";
    }
}

public interface B {
    default String label() {
        return "B";
    }
}

// class C implements A, B {} // ここでコンパイルエラー

エラーメッセージは以下の通りでした。

class C inherits unrelated defaults for label() from types A and B

ダイヤモンド継承問題(複数の親から同じシグネチャのメソッドを継承したときにどちらを使うか曖昧になる問題)です。
解決するには、クラス側で明示的にオーバーライドし、必要ならどちらの実装を使うか指定します。

class C implements A, B {
    @Override
    public String label() {
        return A.super.label() + B.super.label();
    }
}

A.super.label()のように書くことで、特定のinterfaceのデフォルト実装を明示的に呼び出せます。

デフォルトメソッドの設計判断

いつdefaultを使い、いつ抽象メソッドのままにするか

判断軸は「実装クラス間で共通化できる振る舞いかどうか」です。

  • 全実装クラスで共通のロジックになりそう → defaultで共通化
  • 実装クラスごとに本質的に異なるべき振る舞い → 抽象メソッドのまま

例えばComparablecompareToは実装ごとに全く異なるべきなので抽象メソッドのままです。
一方Comparatorreversed()thenComparing()は、既存のcompareロジックを使い回せるのでdefaultで提供されています。

private メソッドとの組み合わせ(Java 9以降)

Java 9からはinterface内にprivateメソッドも定義できるようになりました。
複数のdefaultメソッド間で共通処理を切り出す用途に使えます。

public interface Validator {
    boolean isValid(String value);

    default void requireValid(String value) {
        if (!isValid(value)) {
            throw new IllegalArgumentException(errorMessage(value));
        }
    }

    private String errorMessage(String value) {
        return "invalid value: " + value;
    }
}

errorMessageはinterfaceの外からは呼べず、実装の詳細を隠蔽しつつ内部で再利用できます。

まとめ

この記事のポイント

  • interfaceのdefaultメソッドはJava 8で追加され、既存実装を壊さずにメソッドを拡張できる
  • staticメソッドはファクトリメソッドやユーティリティの置き場として使える
  • 複数interfaceで同名のdefaultメソッドが衝突するとinherits unrelated defaultsエラーになり、A.super.method()で明示解決する
  • Java 9以降はinterface内にprivateメソッドを定義し、defaultメソッド間の共通処理を切り出せる

次に読むべき記事

  • interfaceの基本を復習したい方は「interfaceの基本」の記事へ
  • 抽象クラスとの使い分けは「抽象クラス(abstract)の使い方」の記事へ

タグ: Java, 上級者向け, オブジェクト指向

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