在研讀《瘋狂 Java 講義》時,一段關於繼承與轉型的程式碼讓我陷入深思。程式碼定義了一個抽象父類別 People 和子類別 Student,兩者都擁有同名的實例變數 tag 和 name。透過向上轉型存取父類別的欄位,以及呼叫繼承來的方法,輸出結果卻出乎意料:
package inheritance;
abstract class People {
public String tag = "瘋狂Java講義";
public String name = "Parent";
String getName() { return name; }
}
class Student extends People {
String tag = "輕量級Java EE企業應用實戰";
public String name = "Student";
}
public class HideTest2 {
public static void main(String[] args) {
Student d = new Student();
System.out.println(((People)d).tag); // 輸出:瘋狂Java講義
System.out.println(d.getName()); // 輸出:Parent
}
}
執行結果:
瘋狂Java講義
Parent
許多初學者(包括當時的我)會直覺認為:new Student() 必須先建立 People 對象,但 People 是抽象類別,無法實例化,這似乎產生了矛盾。實際上,子類別實例化不會產生獨立的父類別對象。JVM 僅在子類別對象的記憶體空間內,配置了父類別定義的實例變數(即「繼承下來的欄位」),並呼叫父類別的建構子進行初始化。建構子的角色是「初始化」,而非「產生對象」。new 關鍵字才是產生對象的唯一指令。
一個 Student 對象在堆記憶體中包含兩組同名變數:
People 的 tag、nametag、name(隱藏父類別同名欄位)
這涉及 Java 的兩種綁定機制:
People)決定存取哪個欄位。因此 ((People)d).tag 和 d.getName() 內部存取 name 時,皆綁定至父類別的欄位。Student)決定呼叫哪個方法。若子類別覆寫了方法,則執行子類別版本。在範例中,getName() 未被覆寫,故執行父類別版本,回傳父類別的 name("Parent")。若在 Student 中覆寫 getName() 回傳 this.name,則會輸出 "Student",因為 this 指向子類別對象,存取的是子類別的 name 欄位。
「不會產生父類別對象,只是用了父類別的建構函數而已……變數是靜態綁定,方法是動態綁定。子類別對象內存中有兩份變數,一份繼承自父類別,一份屬於自己。絕對不會產生父類別對象,否則抽象類別豈能實例化?」
深入理解 <init> 初始化方法與 new 指令的區別,能徹底釐清此觀念。子類別建構子首行隱含的 super() 呼叫,只是觸發父類別的初始化邏輯,並非建立父類別對象。