1. 導論
在 Java 中不能同時繼承多個類別。這是刻意的設計,用來避免「鑽石問題」——當兩個父類別定義了同名同簽章的方法時,系統不清楚應該採用哪個實作。不過,介面可以隨你實作多個!為什麼?因為介面就是契約,本身不包含實作(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 —— 標準 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(); // 編譯錯誤
只能建立實作該介面的類別的物件。
GO TO FULL VERSION