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);
    }
}

說明:

  • 介面中的方法預設為 publicabstract,但 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() - 預設版本會運作!
}

現在,如果我們在 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 方法的衝突

如果一個類別實作了兩個介面,而它們各自都有相同簽名的 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();
    }
}

如果沒有在類別 C 中實作 hello(),就會編譯錯誤。

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 方法,你必須明確解決衝突——否則編譯器不會讓你編譯通過。

錯誤 №3:試圖宣告與 Object 方法簽名相同的 default 方法。
不能在介面中做 equalshashCodetoString 的 default 方法——只能宣告這些名稱的抽象方法。

錯誤 №4:忘了 default 方法不是「魔法」,只是方便的工具。
Default 方法不會改變「介面是契約」的原則。如果預設行為不合用——務必在類別中覆寫該 default 方法。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION