許多開發者以為 new 一個物件只是簡單的記憶體配置,但《Think in Java》作者指出:建構子的調用順序蘊含微妙機制,稍有不慎便會導致難以排查的 Bug。本文透過兩個經典案例,層層揭開物件初始化的真實面貌。
class Meal { Meal() { System.out.println("meal"); } }
class Bread { Bread() { System.out.println("Bread"); } }
class Cheese { Cheese() { System.out.println("Cheese"); } }
class Lettuce { Lettuce() { System.out.println("Lettuce"); } }
class Lunch extends Meal { Lunch() { System.out.println("Lunch"); } }
class PortableLunch extends Lunch { PortableLunch() { System.out.println("PortableLunch"); } }
public class Sandwich extends PortableLunch {
private Bread b = new Bread();
private Cheese c = new Cheese();
private Lettuce l = new Lettuce();
public Sandwich() { System.out.println("Sandwich"); }
public static void main(String[] args) { new Sandwich(); }
}
輸出結果:
meal Lunch PortableLunch Bread Cheese Lettuce Sandwich
Object)向下,依序執行各層建構子(meal → Lunch → PortableLunch)。Bread → Cheese → Lettuce。Sandwich 建構子內的列印語句。class Glyph {
void draw() { System.out.println("Glyph draw()"); }
Glyph() {
System.out.println("Glyph before draw();");
draw(); // 動態綁定!
System.out.println("Glyph after draw();");
}
}
class RoundGlyph extends Glyph {
private int radius = 1;
RoundGlyph(int r) {
radius = r;
System.out.println("RoundGlyph(), radius:" + radius);
}
void draw() {
System.out.println("RoundGlyph.draw(), radius:" + radius); // 此處印出 0!
}
}
public class ConstructorTest2 {
public static void main(String[] args) { new RoundGlyph(5); }
}
輸出結果:
Glyph before draw(); RoundGlyph.draw(), radius:0 Glyph after draw(); RoundGlyph(), radius:5
關鍵在於 初始化順序:
radius = 0(預設值)draw()(動態綁定到子類覆寫方法)radius = 1(宣告處賦值)radius = 5(參數賦值)當父類建構子呼叫 draw() 時,子類的 radius 尚未執行第 3 步初始化,仍為預設值 0。這是 在建構子中呼叫非 final/private 方法的經典陷阱。
這些方法無法被覆寫,避免動態綁定導致存取未初始化的成員。若無法避免,務必確保被調用方法不依賴子類尚未初始化的狀態。
物件初始化實際包含四階段:記憶體清零 → 父類建構子遞歸 → 成員初始化 → 本類建構子主體。理解此機制,能有效規避多型與建構子交織產生的隱性 Bug,打牢 Java 進階開發的基石。