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

Java 繼承中構造器調用順序導致字段為 null 的經典問題分析

     編輯:程式設計綜合問答

問題代碼

public class Derived extends Base {
    private String name = "derived";
    public Derived() {
        tellName(); printName();
    }
    public void tellName() { System.out.println("Derived tell name: " + name); }
    public void printName() { System.out.println("Derived print name: " + name); }
    public static void main(String[] args){ new Derived(); }
}

class Base {
    private String name = "base";
    public Base() {
        tellName(); printName();
    }
    public void tellName() { System.out.println("Base tell name: " + name); }
    public void printName() { System.out.println("Base print name: " + name); }
}

運行輸出

Derived tell name: null
Derived print name: null
Derived tell name: derived
Derived print name: derived

核心原因深度解析

1. Java 對象初始化順序

  1. 父類靜態字段/靜態初始化塊 (按聲明順序)
  2. 子類靜態字段/靜態初始化塊
  3. 父類實例字段初始化 / 實例初始化塊
  4. 父類構造器執行 ← 關鍵點
  5. 子類實例字段初始化 / 實例初始化塊
  6. 子類構造器執行

2. 多態方法調用機制

  • 構造器中調用的非 final/private 方法,會動態分發到子類覆寫版本
  • 父類構造器執行時 (步驟 4),子類實例字段 `name` 尚未初始化 (步驟 5 尚未執行),默認值為 `null`
  • 子類覆寫的 `tellName()` 讀取的是子類的 `name` 字段,此時為 `null`

3. 字段隱藏而非覆寫

`private String name` 在父子類各自獨立,不構成覆寫。子類擁有自己的 `name`,父類擁有自己的 `name`。子類方法只能訪問子類的 `name`。

內存狀態演示

階段Base.nameDerived.name調用方法輸出
1. 進入 Base()"base"null (未初始化)Derived.tellName()Derived tell name: null
2. 進入 Derived()"base""derived"Derived.tellName()Derived tell name: derived

最佳實踐避坑指南

❌ 危險做法✅ 安全替代
構造器中調用可覆寫方法將方法標記為 final、private 或 static
依賴子類字段初始化完成通過構造器參數傳遞所需數據
父類構造器啟動線程/註冊監聽提供單獨的 init()/start() 方法,構造後顯式調用
字段隱藏造成困惑使用不同名稱 (如 `baseName`/`derivedName`) 或封裝為 getter

修正後代碼示例

class Base {
    private String name = "base";
    public Base() {
        // 安全:final 方法不可覆寫
        safeTellName(); 
    }
    final void safeTellName() { 
        System.out.println("Base tell name: " + name); 
    }
}

class Derived extends Base {
    private String name = "derived";
    public Derived() {
        super(); // 顯式調用父構造器
        // 此時子類字段已初始化完成
        tellName(); 
    }
    void tellName() { 
        System.out.println("Derived tell name: " + name); 
    }
}

相關規範引用

  • JLS §12.5 Creation of New Class Instances - 詳細規定初始化順序
  • Effective Java Item 19「設計繼承還是設計文檔」- 警告構造器調用可覆寫方法
  • SonarQube Rule S2697「Constructors should not call overridable methods」
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved