這道題源自Sun論壇經典帖子,涉及構造器執行順序、多態調用、字段初始化時機三大核心機制交織。看似簡單的繼承結構,實則暗藏數據竄改風險。本文通過字節碼、JLS規範、實戰案例徹底拆解。
public class Parent {
public void test() {} // 可被覆寫
public Parent() {
test(); // 構造器中調用可覆寫方法 → 危險!
}
public static void main(String[] args) {
new Child();
}
}
class Child extends Parent {
private int instanceValue = 20; // 實例字段顯式初始化
@Override
public void test() {
System.out.println("instance value is: " + instanceValue);
}
}
輸出:instance value is: 0 (而非預期的20)
int→0、引用→null)Child()隱式首行super()Object → ... → Parent構造器test()instanceValue = 20 在此處執行!| 階段 | instanceValue值 | test()可見值 |
|---|---|---|
| 默認初始化完成 | 0 | 0 |
| Parent構造器體執行(調用test) | 0 | 0 ← 多態調用Child.test(),讀到默認值 |
| Child字段顯式初始化 | 20 | 20 |
| Child構造器體執行 | 20 | 20 |
// Child. 關鍵片段
0: aload_0
1: invokespecial #Parent. // 調用父構造器
// 父構造器內:
// aload_0
// invokevirtual #Child.test // 多態調用!
// return
4: aload_0
5: bipush 20
6: putfield #instanceValue // 顯式初始化在父構造器返回後!
9: return
可見:putfield instanceValue在invokespecial Parent.之後,而父構造器內已通過invokevirtual調用了子類test()。
test()可能依賴字段進行業務邏輯(如註冊監聽器、啟動線程、建立連接),讀到默認值導致NPE、邏輯錯誤、資源洩漏
public class Parent {
public Parent() {
init(); // private/final/static方法,不可覆寫
}
private void init() { ... } // 或 final void init() / static void init()
}
public class Parent {
protected Parent() {} // 防止直接new
public static Parent create() {
Parent p = new Child();
p.initialize(); // 顯式初始化,構造器完成後調用
return p;
}
protected void initialize() {} // 子類覆寫
}
Spring等容器先構造對象(字段默認值),再注入依賴,再調用@PostConstruct初始化,天然避開此坑。
@implSpec 构造器调用了可覆寫方法X,子類覆寫時不得依賴尚未初始化的字段Error Prone、SpotBugs檢測ConstructorCallsOverridableMethodclone()調用super.clone()後,字段已復制,但若clone()內調用可覆寫方法,同理有風險readObject()執行時字段已恢復,但readObjectNoData()、validateObject()調用可覆寫方法需謹慎this::method,若method可覆寫,捕獲this引用洩露,同類問題構造器中調用可覆寫方法 = 多態洩露半初始化對象。這是Java繼承模型的固有缺陷,語言層面無法禁止(違背多態語義),只能靠開發者紀律、靜態分析、設計模式規避。鐵律:構造器只調用private/final/static方法,或不調用任何實例方法。下次Code Review見到super()後調用非final方法,請果斷駁回。