看似簡單的繼承代碼,運行結果卻違背直覺。這是《Java Puzzlers》中的經典陷阱,揭示了 Java 對象初始化順序的深層機制。
public class Base {
Base() { preProcess(); }
void preProcess() {}
}
public class Derived extends Base {
public String whenAmISet = "set when declared";
@Override void preProcess() {
whenAmISet = "set in preProcess()";
}
}
public class Main {
public static void main(String[] args) {
Derived d = new Derived();
System.out.println(d.whenAmISet); // 輸出什麼?
}
}
直覺預期:"set in preProcess()"
實際輸出:"set when declared"
Java 對象創建的完整順序(以 new Derived() 為例):
Derived 構造器(隱式 super() 前)Base() 構造器Base 構造器調用 preProcess()Derived.preProcess(),設置 whenAmISet = "set in preProcess()"whenAmISet = "set when declared" ← 關鍵覆蓋點Derived 構造器體(本例為空)錯誤假設:成員變量聲明與初始化是原子操作。
事實真相:Java 將「聲明(分配內存+默認值)」與「顯式初始化」分離,初始化發生在父類構造器完成之後、子類構造器體之前。
Java 強制父類構造器先於子類成員初始化完成,保證子類可安全使用父類已初始化的狀態。若子類成員先初始化,父類構造器中調用被覆寫方法訪問子類字段時將讀到未初始化狀態。
| 語言 | 構造器中調用虛函數行為 | 設計考量 |
|---|---|---|
| C++ | 調用當前類版本(基類構造時調用基類版本) | 避免訪問未初始化的派生類成員,安全優先 |
| Java | 始終調用最派生類版本(動態綁定) | 多態一致性優先,但要求開發者避免在構造器調用可覆寫方法 |
C++ 禁止成員聲明處初始化,強制在構造器初始化列表顯式初始化,從語法層面規避此問題;Java 允許聲明處初始化,靈活性換取了認知負擔。
preProcess() 標記 final、private 或 staticJoshua Bloch & Neal Gafter 合著 **《Java Puzzlers: Traps, Pitfalls, and Corner Cases》** (中文版《Java 解惑》) 第 45 題 "Initialization Order" 深度剖析此陷阱,是每位 Java 開發者的必讀經典。
「聲明 ≠ 初始化」、「父類構造器調用子類覆寫方法時子類字段尚未顯式初始化」—— 理解這兩點,即可避開 Java 最隱蔽的初始化陷阱。記住:構造器只做賦值,不做業務;多態留給構造器之後。