「對象在構造器執行後創建」是普遍誤區。實則對象實例在**進入構造器前已完成分配與零值初始化**。本文通過代碼實證、字節碼分析、繼承鏈解剖,還原對象誕生全過程,避開「構造器調用可覆寫方法導致字段未初始化」等致命坑。
Date date = new Date();
System.out.println(date.getTime()); // 能調用方法,說明對象已存在
誤以為 `new Date()` 返回時對象才誕生。實則 `new` 指令包含**分配內存、零值初始化、設置對象頭、調用構造器**四步,構造器只是最後一步。
public class Test {
public Test() {
this.doSomething(); // 構造器內調用實例方法,證明 this 已指向有效對象
}
private void doSomething() { System.out.println("do init"); }
}
構造器第一行隱含 `super()` 前,對象已在堆中分配完成,`this` 引用已就緒。
public class Base {
public Base() { init(); }
protected void init() { System.out.println("Base init"); }
}
public class Sub extends Base {
private int i = 1024;
private String s = "13 leaf";
public Sub() {
System.out.println("Sub constructor");
init();
}
@Override
protected void init() {
System.out.println("Sub init: i=" + i + ", s=" + s); // 此時 i=0, s=null !
}
public static void main(String[] args) { new Sub(); }
}
輸出順序:
Base init Sub init: i=0, s=null ← 危險!子類字段尚未初始化 Sub constructor Sub init: i=1024, s=13 leaf
public Sub();
aload_0 // this
invokespecial Base. // 1. 調用父構造器 (父構造器內調用 init() → 多態調用 Sub.init())
aload_0 // this
sipush 1024 // 2. 子類字段 i 賦值
putfield Sub.i : int
aload_0
ldc "13 leaf" // 3. 子類字段 s 賦值
putfield Sub.s : java.lang.String
getstatic System.out
ldc "Sub constructor"
invokevirtual PrintStream.println
aload_0
invokevirtual Sub.init() // 4. 子類構造器代碼體調用 init()
return
鐵證:字段賦值 (`putfield`) 在父構造器返回**之後**、子類構造器代碼**之前**執行。父構造器調用可覆寫方法時,子類字段**尚未賦值**,仍為零值!
// 父類
public Base() {
initialize(); // 危險!多態調用子類方法
}
protected void initialize() {}
// 子類
public class Sub extends Base {
private final List items = new ArrayList<>(); // 字段初始化
@Override
protected void initialize() {
items.add("init"); // NPE! items 尚未賦值,仍為 null
}
}
規則:構造器 (含父類構造器) **絕不調用可覆寫方法** (非 `final`/`private`/`static`)。若需初始化邏輯複用,用 `private` 輔助方法或構造器內直接寫代碼。
| 場景 | 風險 | 防禦措施 |
|---|---|---|
| 父類構造器調用可覆寫方法 | 子類字段未初始化 → NPE/邏輯錯誤 | 方法標 `final`/`private`;或改用工廠方法/初始化模板模式 (延遲到構造器後調用) |
| 子類構造器前使用字段 | 讀取零值而非預期值 | 字段聲明處初始化;構造器首行賦值;避免在字段初始化表達式中調用可覆寫方法 |
| 循環繼承/鑽石繼承初始化 | 字段重複賦值/順序混亂 | 組合優於繼承;介面默認方法替代部分需求 |
| 序列化/反射創建對象 | 繞過構造器,字段零值 | 實現 `readObject`/`readResolve`;反射調用構造器而非 `Unsafe.allocateInstance` |
// Java 16+ Record: 編譯器自動生成構造器、字段、equals/hashCode/toString
public record User(String name, int age, LocalDate birthday) {
// 緊湊構造器:驗證邏輯,無需手動賦值字段
public User {
if (age < 0) throw new IllegalArgumentException("age >= 0");
if (birthday == null) throw new IllegalArgumentException("birthday required");
}
}
// 或 Lombok @Value / 手寫不可變類
@Value
public class User {
String name;
int age;
LocalDate birthday;
}
記錄類/不可變類**無可覆寫方法、字段 `final`、單一構造器**,從根源消除構造器初始化順序陷阱,編譯器保證線程安全發布。
對象實例在 **`new` 指令分配內存、零值初始化完成瞬間誕生**,構造器僅負責「從零值到業務初始值」的賦值過程。**父類構造器執行期間,子類字段均為零值**,切勿在構造器鏈中觸發多態調用。擁抱記錄類、不可變模式、工廠方法,讓對象創建回歸簡單、安全、可推理。