CodeGym /課程 /JAVA 25 SELF /介面的多重實作

介面的多重實作

JAVA 25 SELF
等級 20 , 課堂 2
開放

1. 導論

在 Java 中不能同時繼承多個類別。這是刻意的設計,用來避免「鑽石問題」——當兩個父類別定義了同名同簽章的方法時,系統不清楚應該採用哪個實作。不過,介面可以隨你實作多個!為什麼?因為介面就是契約,本身不包含實作(Java 8 之前沒有實作;自 Java 8 起允許有 defaultstatic 方法)。因此不會產生繼承實作上的混亂。

這很像現實生活:你可以同時是「駕駛員」、「電腦使用者」與「游泳者」。每個「介面」描述的是一組能力,但不會要求你成為另一個人的複本。

多重介面實作的語法

在 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 —— 標準 Java 程式庫中就有這個介面)
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. 特性與限制

每個方法只需一個實作

如果兩個介面定義了相同簽章的方法,只需要實作一次。範例:

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)。因此不會出現「兩個父類別都有同名欄位」的混亂。

4. 範例:在一個類別中實作多個介面

讓我們為教學應用程式(例如動物園)新增一些功能。假設有能移動並能發出聲音的動物:

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("Reks");
    Cat murka = new Cat("Murka");

    testMovable(rex);       // Reks 跑向 (10, 20)
    testSoundable(murka);   // Murka 說:喵!
}

當然,如果物件同時實作兩個介面,就可以同時傳給兩種方法!

6. 實用細節

如果介面彼此衝突?

有時兩個介面會定義簽章相同、但語意不同的方法。比如說,一個介面預期 reset() 會重設座標,另一個介面則預期相同方法會關閉裝置。這種情況下你必須留意:方法仍然只能實作一次,而且它要能同時滿足兩種需求(或至少做出取捨)。現實中這種情況不常見,但一旦遇到,就應該檢視設計是否合理。

含有不同介面物件的集合範例

假設我們有一個包含實作不同介面的物件清單。我們可以將它們遍歷並呼叫需要的方法:

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

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

同樣的作法也適用於任何介面。

7. 多重實作介面時的常見錯誤

錯誤 1:沒有實作介面中的所有方法。
如果類別宣告要實作某個介面,但少實作了其中任一方法——編譯器會立刻報錯。請不要遺漏任何方法,即使它們看起來「多餘」。

錯誤 2:相同簽章但語意衝突的方法。
如果兩個介面定義了相同的方法,只需要實作一次。但如果這些方法的語意不同,可能會造成混淆與 bug。這種情況下最好重新思考整體架構。

錯誤 3:在類別中嘗試用 extends 來繼承介面。
在類別中要實作介面,必須使用 implements,而不是 extends。例如:

public class MyClass implements A, B { ... } // 正確
public class MyClass extends A, B { ... }    // 錯誤!

錯誤 4:嘗試直接建立介面的物件。
介面是契約,不能直接建立實例:

Movable m = new Movable(); // 編譯錯誤

只能建立實作該介面的類別的物件。

1
任務
JAVA 25 SELF, 等級 20, 課堂 2
上鎖
數位畫布:繪製與擦除
數位畫布:繪製與擦除
1
任務
JAVA 25 SELF, 等級 20, 課堂 2
上鎖
使用者檔案:名稱與可序列化能力
使用者檔案:名稱與可序列化能力
1
任務
JAVA 25 SELF, 等級 20, 課堂 2
上鎖
通用操作:單一按鈕控制一切
通用操作:單一按鈕控制一切
1
任務
JAVA 25 SELF, 等級 20, 課堂 2
上鎖
媒體中心:播放與錄製
媒體中心:播放與錄製
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION