問題代碼
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 對象初始化順序
- 父類靜態字段/靜態初始化塊 (按聲明順序)
- 子類靜態字段/靜態初始化塊
- 父類實例字段初始化 / 實例初始化塊
- 父類構造器執行 ← 關鍵點
- 子類實例字段初始化 / 實例初始化塊
- 子類構造器執行
2. 多態方法調用機制
- 構造器中調用的非 final/private 方法,會動態分發到子類覆寫版本
- 父類構造器執行時 (步驟 4),子類實例字段 `name` 尚未初始化 (步驟 5 尚未執行),默認值為 `null`
- 子類覆寫的 `tellName()` 讀取的是子類的 `name` 字段,此時為 `null`
3. 字段隱藏而非覆寫
`private String name` 在父子類各自獨立,不構成覆寫。子類擁有自己的 `name`,父類擁有自己的 `name`。子類方法只能訪問子類的 `name`。
內存狀態演示
| 階段 | Base.name | Derived.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」