首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

Java 對象初始化順序陷阱:父類構造器調用子類字段賦值導致「賦值失效」完整復現與解析

     編輯:Java 綜合教學

現象復現


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  ← 正常

根因深度解析:JLS §12.5 初始化順序鐵律

  1. 主線程調用 new Lower()
  2. 分配內存、字段默認值初始化:
    upperString = null, lowerString = null (此時所有字段為默認值)
  3. 調用父類構造器 Upper() (語言強制)
  4. 執行 Initializer.initialize(this):
    此時 this 已是 Lower 實例,instanceof 成立,
    成功將 lowerString 設為 "lowerInited"
  5. 父類構造器結束,返回子類構造器
  6. ⚡ 關鍵步驟:子類實例初始化器 (Instance Initializers) 執行
    JLS 規定:在子類構造器體執行前,按聲明順序執行字段顯式賦值與實例初始化塊。
    String lowerString = null; 這行顯式賦值屬於實例初始化器,會在步驟 6 執行,將剛剛設好的 "lowerInited" 覆蓋回 null!
  7. 執行子類構造器體:打印時 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、不調用可覆寫方法、字段聲明不寫冗餘默認值。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved