CodeGym /コース /JAVA 25 SELF /インターフェースの多重実装

インターフェースの多重実装

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

1. はじめに

Java では複数のクラスを同時に継承することはできません。これは、2つの親クラスが同じメソッドを定義している場合に、どの実装を使うべきか曖昧になる「ダイヤモンド問題」を避けるためです。しかし、インターフェースは好きなだけ実装できます。なぜでしょうか? インターフェースはあくまで契約(コンタクト)であり、実装を持たないためです(Java 8 以前は実装なし。Java 8 以降は default メソッドや static メソッドはあり得ます)。したがって、コード継承の混乱は発生しません。

これは現実世界の状況にも似ています。あなたは同時に「運転手」「コンピュータユーザー」「水泳選手」であり得ます。これらの「インターフェース」はそれぞれ特定のスキルを記述しますが、あなたを誰かのコピーにすることはありません。

複数インターフェース実装の構文

Java では、クラスはキーワード implements の後にカンマ区切りでインターフェースを列挙することで、複数のインターフェースを実装できます。基本例は次のとおりです。

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

public interface Chargeable {
    void charge();
}

public class Robot implements Movable, Chargeable {
    @Override
    public void move(int x, int y) {
        System.out.println("ロボットが (" + x + ", " + y + ") に移動します");
    }

    @Override
    public void charge() {
      System.out.println("ロボットが充電しています。");
    }
}

この例の Robot は万能選手で、移動も充電もできます。現実と同じで、できることが多いほど面接に呼ばれやすいのです!

2. 何のために? 実用例

例1. オブジェクトの異なる「役割」

ゲームのキャラクターを設計しているとします。

  • 移動できる(Movable
  • 攻撃できる(Attackable
  • ファイルに保存できる(Serializable — 標準ライブラリにあるインターフェース)
public interface Attackable {
    void attack();
}

public class Hero implements Movable, Attackable, java.io.Serializable {
    @Override
    public void move(int x, int y) {
        System.out.println("ヒーローは新しい位置に移動します。");
    }

    @Override
    public void attack() {
        System.out.println("ヒーローが攻撃します!");
    }
}

これで、クラスはさまざまな文脈で使えるようになります。これらのいずれかのインターフェースを要求するメソッドに渡すことができます。

例2. 標準インターフェースの組み合わせ

Java 標準ライブラリでよく見かけるインターフェースに Comparable(オブジェクトの比較)と Serializable(ファイル保存やネットワーク送信)があります。ときには、オブジェクトがその両方である必要があります。

public class Person implements Comparable<Person>, java.io.Serializable {
    private String name;
    private int age;

    public Person(String name, int age) { this.name = name; this.age = age; }

    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age);
    }
}

これで Person オブジェクトは(たとえばリスト内で)並べ替えられ、ファイルにも書き出せます。

3. 特徴と制約

同じシグネチャのメソッドは1回の実装でOK

2つのインターフェースが同じシグネチャのメソッドを定義している場合、その実装は1回でかまいません。例:

public interface A {
    void doSomething();
}
public interface B {
    void doSomething();
}
public class MyClass implements A, B {
    @Override
    public void doSomething() {
        System.out.println("両方のインターフェース向けの doSomething の実装です。");
    }
}

Java はエラーにしません——シグネチャが一致していれば問題ありません。もしメソッドがシグネチャの面で異なっていれば、それらは別メソッドと見なされるため、それぞれ実装する必要があります。

いわゆるダイヤモンド問題は起きない

クラスの多重継承と異なり、複数のインターフェースを実装しても、同じメソッドに対して異なる実装が継承される状況は起きません。Java 8 以前、インターフェースには実装がありませんでしたし、default メソッドが導入された後でも、衝突が起きた場合は明示的に解決する義務があります(この点は次の講義で詳しく扱います)。

状態は持たない

インターフェースは通常のフィールドを持てません(定数のみ — public static final)。したがって、「同名の親フィールドが2つ」という混乱は発生しません。

4. 例: 1つのクラスで複数インターフェースを実装する

学習用アプリ(たとえば動物園)に新機能を追加しましょう。動物は移動でき、鳴き声を出せるものとします。

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

public interface Soundable {
    void makeSound();
}

public class Dog implements Movable, Soundable {
    private String name;

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

    @Override
    public void move(int x, int y) {
        System.out.println(name + " が (" + x + ", " + y + ") に走っていく");
    }

    @Override
    public void makeSound() {
        System.out.println(name + " が言う: ワンワン!");
    }
}

public class Cat implements Movable, Soundable {
    private String name;

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

    @Override
    public void move(int x, int y) {
        System.out.println(name + " が (" + x + ", " + y + ") へ忍び寄る");
    }

    @Override
    public void makeSound() {
        System.out.println(name + " が言う: ニャー!");
    }
}

次のように、「移動できる」「音を出せる」あらゆるオブジェクトを扱う汎用メソッドを書けます。

public static void testMovable(Movable m) {
    m.move(10, 20);
}

public static void testSoundable(Soundable s) {
    s.makeSound();
}

public static void main(String[] args) {
    Dog rex = new Dog("Rex");
    Cat murka = new Cat("Murka");

    testMovable(rex);       // Rex が (10, 20) に走っていく
    testSoundable(murka);   // Murka が言う: ニャー!
}

もちろん、オブジェクトが両方のインターフェースを実装していれば、どちらにも渡せます。

6. 便利なニュアンス

インターフェースが衝突する場合は?

2つのインターフェースが同じシグネチャのメソッドを定義していても、意味が異なることがあります。たとえば、あるインターフェースは reset() メソッドが座標のリセットを行うことを期待し、別のインターフェースは同じメソッドでデバイスの電源を切ることを期待するといった具合です。この場合でもメソッドは1回だけ実装する必要があり、両方の振る舞いに対応するか、少なくともどちらを採用するかを選ばなければなりません。現実にはまれですが、遭遇したら設計の妥当性を見直すべきサインです。

異なるインターフェースを実装するオブジェクトのコレクション例

複数のインターフェースを実装したオブジェクトのリストがあるとします。走査して必要なメソッドを呼び出せます。

Movable[] movables = {
    new Dog("Sharik"),
    new Cat("Barsik"),
    new Robot()
};

for (Movable m : movables) {
    m.move(0, 0);
}

同様のことは、どのインターフェースに対しても行えます。

7. 複数インターフェース実装でよくあるミス

エラー1: インターフェースのメソッドをすべて実装していない。
クラスがインターフェースを実装すると宣言したのに、1つでもメソッドを実装していなければ、コンパイラは即座にエラーを出します。「余計」に見えるメソッドでも忘れないでください。

エラー2: 同じシグネチャだが意味が競合するメソッド。
2つのインターフェースが同じメソッドを定義している場合、実装は1回で足ります。しかし、その意味が異なると混乱やバグの原因になります。こうした場合は設計を見直すのが賢明です。

エラー3: クラスで extends によってインターフェースを継承しようとする。
クラスがインターフェースを実装する場合は常に implements を使い、extends ではありません。例:

public class MyClass implements A, B { ... } // 正しい
public class MyClass extends A, B { ... }    // エラー!

エラー4: インターフェースのインスタンスを生成しようとする。
インターフェースは契約であり、直接生成することはできません。

Movable m = new Movable(); // コンパイルエラー

生成できるのは、そのインターフェースを実装するクラスのオブジェクトだけです。

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