CodeGym /コース /JAVA 25 SELF /インターフェースのdefaultメソッド

インターフェースのdefaultメソッド

JAVA 25 SELF
レベル 21 , レッスン 2
使用可能

1. はじめに

かつて(Java 8 以前)インターフェースはとても厳格で、宣言できるのは実装のない抽象メソッドと定数(public static final)だけでした。これは便利でしたが、やがて大きな問題が生まれました。ライブラリの進化です。

状況を想像してください

あなたは人気のライブラリを開発し、その中に次のようなインターフェースがあります:

public interface Movable {
    void move(int x, int y);
}

世界中の何千人ものプログラマがこのインターフェースを実装するクラスを書いています。数年後、みんなにreset()という、オブジェクトを初期位置に戻すメソッドが足りないことに気づきます。あなたはインターフェースに次を追加します:

public interface Movable {
    void move(int x, int y);
    void reset();
}

すると悪夢が始まります。あなたのインターフェースを使っているすべてのプロジェクトがコンパイルできなくなるのです! 新しいメソッドを必ず実装しなければならなくなりましたが、その存在は誰も知りませんでした。移行は苦痛に変わります。

defaultメソッドが解決策!

Java 8 ではdefaultメソッドが導入され、インターフェースに実装つきのメソッドを追加できるようになりました。既存のクラスは自動的に標準実装を得るため、コードは壊れません。必要であれば、クラス側で自分のやり方にオーバーライドできます。

2. defaultメソッドの構文

default メソッドは、インターフェース内に実装を持ち、キーワードdefaultが付いた通常のメソッドです。

public interface Movable {
    void move(int x, int y);

    default void reset() {
        // 典型的な実装: 原点に戻す
        move(0, 0);
    }
}

補足:

  • インターフェースのメソッドは既定でpublicかつabstractですが、default メソッドは抽象ではなく本体を持ちます。
  • キーワードdefaultは常にメソッドの戻り値の型の前に書きます。

クラスではどう見える?

public class Robot implements Movable {
    private int x, y;

    @Override
    public void move(int x, int y) {
        this.x = x;
        this.y = y;
        System.out.println("ロボットは (" + x + ", " + y + ") に移動しました");
    }

    // reset() を実装する必要はありません。default 版が動作します!
}

さて、Robotオブジェクトでreset()を呼ぶと、Movableインターフェースにある実装が使われます:

public class Main {
    public static void main(String[] args) {
        Movable robot = new Robot();
        robot.move(10, 20); // ロボットは (10, 20) に移動しました
        robot.reset();      // ロボットは (0, 0) に移動しました
    }
}

3. 標準ライブラリにおける default メソッド

default メソッドは、巨大な標準インターフェース群を既存コードを壊さずに進化させるために導入されました。

例: インターフェース List(Java 8+)

Java 8 ではインターフェースListに実装つきメソッド、例えばforEachreplaceAllsortが追加されました:

default void forEach(Consumer<Entity> action) {
    for (Entity e : this) {
        action.accept(e);
    }
}

自作のリストを実装していてforEachをオーバーライドしていなくても、default メソッドのおかげで動作します。

ジェネリック型(Consumer<Entity>)の詳細は 26 レベルで学びます :P

4. default メソッドはなぜ必要?

  • コードを壊さない API の進化:インターフェースに新しいメソッドを追加しても、既存のすべてのクラスで実装する必要がありません。
  • 汎用的な振る舞いテンプレート:既定の振る舞いを宣言し、クラスはそれを利用したりオーバーライドしたりできます。
  • 重複の削減:ほとんどの実装で同じ振る舞いなら、各クラスにコードをコピペする必要はありません。

たとえ話

賃貸契約(インターフェース)があるとします。以前は「借主は水道代を支払うこと」と書かれていました。その後「借主は電気代を支払うこと」を追加しました。もし default メソッドがなければ、すべての借主との契約を書き換える必要があったでしょう。しかし default メソッドがあれば、条項を追加するだけで、必要な場合は各自で別の取り決め(クラスでのオーバーライド)ができます。

5. default メソッドの制約と注意点

default メソッドはクラス Object のメソッドをオーバーライドできない

インターフェースで、ObjectequalshashCodetoStringと同じシグネチャの default メソッドを宣言することはできません。これは混乱を防ぐためです。Java のあらゆるオブジェクトはすでにこれらのメソッドを持っているからです。

// コンパイルエラー!
interface Broken {
    default boolean equals(Object obj) { return false; }
}

default メソッドの衝突

もしクラスが2つのインターフェースを実装し、それぞれに同じシグネチャの default メソッドがあるとしたら? Java コンパイラは正直にこう言います。「自分で解決して。どうすればいいか分からない!」

interface A {
    default void hello() { System.out.println("Hello from A"); }
}

interface B {
    default void hello() { System.out.println("Hello from B"); }
}

class C implements A, B {
    // 衝突を必ず解決すること:
    @Override
    public void hello() {
        // どちらのメソッドを呼ぶか選ぶか、独自実装にする
        A.super.hello(); // または B.super.hello();
    }
}

クラスChello()を実装しない場合、コンパイルエラーになります。

default メソッドはインターフェース内の他メソッドを呼び出せる

default メソッドは、たとえ抽象メソッドであっても同じインターフェース内の他メソッドを呼び出せます。重要なのは、クラス側に実装があることです。

interface Printer {
    void print(String text);

    default void printTwice(String text) {
        print(text);
        print(text);
    }
}

6. 例: default メソッドでアプリを進化させる

インターフェースMovableでの default メソッドの使い方を見てみましょう:

public interface Movable {
    void move(int x, int y);

    default void reset() {
        move(0, 0);
    }
}

このインターフェースを実装するクラスRobotがあります:

public class Robot implements Movable {
    private int x = 5;
    private int y = 7;

    @Override
    public void move(int x, int y) {
        this.x = x;
        this.y = y;
        System.out.println("ロボットは (" + x + ", " + y + ") に移動しました");
    }

    // reset() は実装せず、default メソッドを使います!
}

では両方のメソッドを呼んでみます:

public class Main {
    public static void main(String[] args) {
        Movable robot = new Robot();
        robot.move(10, 20); // ロボットは (10, 20) に移動しました
        robot.reset();      // ロボットは (0, 0) に移動しました
    }
}

もしRobotのリセット方法を特別にしたいなら、クラスでreset()をオーバーライドするだけです:

@Override
public void reset() {
    System.out.println("ロボットをシャットダウンして基地に戻ります!");
    move(0, 0);
}

7. default メソッドと複数インターフェース実装

クラスが複数のインターフェースを実装する場合、default メソッドは特に有用です。ただし注意点があります。両方のインターフェースに同じシグネチャの default メソッドがあると、コンパイラは衝突の明示的な解決を求めます。

衝突の例

interface A {
    default void show() { System.out.println("A"); }
}
interface B {
    default void show() { System.out.println("B"); }
}
class C implements A, B {
    @Override
    public void show() {
        // どちらの default メソッドを使うか明示的に選ぶ
        A.super.show(); // または B.super.show();
    }
}

8. 図解: default メソッド呼び出しの仕組み


+-------------------+
|   Movable         |
|-------------------|
| +move(int, int)   | <- 抽象メソッド
| +reset()          | <- default メソッド
+-------------------+
         ^
         |
+-------------------+
|   Robot           |
|-------------------|
| +move(int, int)   | <- 実装している
|                   | (reset() は未実装)
+-------------------+
         |
     reset() の呼び出し
         |
   実装が使用される
   Movable インターフェースのもの
default メソッドの呼び出し: インターフェースのデフォルト実装が使われる

9. default メソッドでありがちなミス

ミス1: 実装なしの default メソッドを定義しようとする。
default メソッドには必ず本体が必要です! もしdefault void foo();と書いたら、コンパイラはすぐに「波かっこを忘れていませんか?」と言うでしょう。

ミス2: 異なるインターフェース間の default メソッドの衝突。
クラスが同じ default メソッドを持つ2つのインターフェースを実装している場合、衝突は明示的に解決しなければなりません。そうしないとコンパイルできません。

ミス3: Object のメソッドと同じシグネチャの default メソッドを宣言しようとする。
インターフェースでequalshashCodetoStringの default メソッドは作れません。これらは抽象メソッドとしてのみ宣言できます。

ミス4: default メソッドは「魔法」ではなく、単なる便利な手段だということを忘れる。
default メソッドでも、インターフェースが契約であるという原則は変わりません。既定の振る舞いが合わないなら、クラスで default メソッドをオーバーライドしてください。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION