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() - 預設版本會運作!
}
現在,如果我們在 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 新增了帶有實作的方法,例如,forEach、replaceAll、sort:
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 的方法
你不能在介面中宣告與 Object 的 equals、hashCode 或 toString 相同簽名的 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 中的實作
9. 使用 default 方法時的常見錯誤
錯誤 №1:嘗試宣告沒有實作的 default 方法。
Default 方法必須要有方法本體!如果你寫成 default void foo();,編譯器會立刻說:「你是不是忘了大括號?」
錯誤 №2:來自不同介面的 default 方法衝突。
如果類別實作了兩個介面,且它們擁有相同的 default 方法,你必須明確解決衝突——否則編譯器不會讓你編譯通過。
錯誤 №3:試圖宣告與 Object 方法簽名相同的 default 方法。
不能在介面中做 equals、hashCode 或 toString 的 default 方法——只能宣告這些名稱的抽象方法。
錯誤 №4:忘了 default 方法不是「魔法」,只是方便的工具。
Default 方法不會改變「介面是契約」的原則。如果預設行為不合用——務必在類別中覆寫該 default 方法。
GO TO FULL VERSION