class Upper {
String upperString;
public Upper() {
Initializer.initialize(this); // 父構造器中回調
}
}
class Lower extends Upper {
String lowerString = null; // 顯式賦值 null
public Lower() {
super();
System.out.println("Upper: " + upperString);
System.out.println("Lower: " + lowerString);
}
}
class Initializer {
static void initialize(Upper u) {
if (u instanceof Lower) {
Lower l = (Lower) u;
l.lowerString = "lowerInited"; // 試圖初始化子類字段
}
u.upperString = "upperInited";
}
}
// 輸出:
// Upper: upperInited
// Lower: null ← 明明已賦值 "lowerInited",為什麼變回 null?
= null 即正常
class Lower extends Upper {
String lowerString; // 無顯式賦值
// ...
}
// 輸出:
// Upper: upperInited
// Lower: lowerInited ← 正常
new Lower()upperString = null, lowerString = null (此時所有字段為默認值)Upper() (語言強制)Initializer.initialize(this):this 已是 Lower 實例,instanceof 成立,lowerString 設為 "lowerInited"String lowerString = null; 這行顯式賦值屬於實例初始化器,會在步驟 6 執行,將剛剛設好的 "lowerInited" 覆蓋回 null!lowerString 已是 null。
new Lower()
│
├─► 內存分配 + 默認值 (upper=null, lower=null)
│
├─► Upper() 構造器
│ └─► Initializer.initialize()
│ └─► lowerString = "lowerInited" ✅ 生效
│
├─► 【子類實例初始化器】 ← 這裡是分水嶺
│ └─► lowerString = null ❌ 覆蓋!(僅當顯式寫 = null 時)
│
└─► Lower() 構造器體
└─► 打印 lowerString → null
= null 就正常?String lowerString; 不產生實例初始化器字節碼,步驟 6 無操作,父類構造器中的賦值得以保留。String lowerString = null; 編譯器生成 putfield 指令於 <init> 方法中 (父 super() 調用後、構造器體前),強制覆蓋。本質:= null 雖與默認值相同,但語法上屬於顯式初始化,觸發 JLS 強制執行順序。
| 反模式 | 風險 | 修正 |
|---|---|---|
| 父類構造器調用可覆寫方法/回調子類 | 子類字段未初始化、狀態不一致 | 構造器中只調用 final/private/static 方法;回調移至初始化方法 (Init pattern) |
子類字段顯式賦值默認值 (= null, = 0, = false) | 掩蓋父類/回調中的初始化邏輯 | 移除冗餘賦值,依賴默認值語義;或統一在構造器體賦值 |
| 依賴字段聲明順序進行複雜初始化 | 可讀性差、易重構出錯 | 集中在構造器體、工廠方法或 Builder 中初始化 |
Java 對象初始化順序是「默認值 → 父類構造器 → 子類字段顯式賦值/初始化塊 → 子類構造器體」。
父類構造器中對子類字段的賦值,會被後續的子類顯式賦值 (= null) 覆蓋。
這不僅是面試題,更是框架設計 (如 Spring 依賴注入、模板方法模式) 必須遵守的鐵律。
鐵律:構造器保持簡單、不洩露 this、不調用可覆寫方法、字段聲明不寫冗餘默認值。