許多 Java 入門書都說:「基本型別存在堆疊,物件存在堆積」。但當你實際執行以下程式碼時,會發現事情沒那麼簡單:
public class Test {
public static void main(String[] args) {
String[] names = { "Michael", "Orson", "Andrew" }; // 靜態初始化,長度 3
String[] animal = new String[4]; // 動態初始化,長度 4
names = animal; // 將 names 指向 animal 所參考的陣列
System.out.println(names.length); // 輸出 4
System.out.println(animal.length); // 輸出 4
}
}
明明 names 原本長度是 3,animal 是 4,賦值後兩者長度同為 4。這是否意味著「陣列大小改變了」?答案是否定的——這揭示了引用變數與陣列物件的本質區別。
names 和 animal 只是引用變數(類似 C/C++ 的指標),它們儲存在堆疊中,佔據固定大小的記憶體空間(存放記憶體位址)。真正的陣列物件(包含長度、元素資料)建立在堆積上,大小一經確定不可變更。
names = animal; 只是讓 names 這個引用變數複製了 animal 手中的「地址」,兩個變數現在指向同一個堆積上的陣列物件。原本 names 指向的長度為 3 的陣列物件因無人引用,將被 GC 回收。
藍虛線:賦值前;紅線:賦值後。兩變數共同指向同一堆積物件,原物件成垃圾。
陣列元素若為基本型別(如 int[]),值直接儲存在陣列物件內部(堆積上);若為參考型別(如 String[]),元素儲存的是指向堆積上 String 物件的引用。圖中 "Michael" 等字串常數同樣駐留在堆積(字串常數池)。
區域變數(含基本型別變數、引用變數)存放在堆疊;引用變數所指向的物件(陣列、類實例)存放在堆積。
Java 堆積上的物件無法直接訪問,必須透過堆疊上的引用變數作為「中介」。只有當引用變數呼叫物件方法或存取屬性時(如 dog.info()、animal[0].info()),才真正操作到堆積上的實體物件。
public class Animal {
private String name;
private int age;
public Animal(String name, int age) { this.name = name; this.age = age; }
public void info() { System.out.println(name + " " + age); }
}
public class Test {
public static void main(String[] args) {
Animal[] animal = new Animal[2]; // 堆積建立長度 2 的 Animal 陣列(元素初值 null)
Animal cat = new Animal("cat", 1); // 堆積建立 cat 物件,堆疊 cat 變數存引用
Animal dog = new Animal("dog", 2); // 堆積建立 dog 物件,堆疊 dog 變數存引用
animal[0] = dog; // animal[0] 與 dog 指向同一物件
animal[1] = cat; // animal[1] 與 cat 指向同一物件
System.out.println(animal.length); // 2
dog.info(); // dog 2
animal[0].info(); // dog 2 同一物件
}
}
藍線:賦值前;紅虛線:賦值後。animal[0]與dog、animal[1]與cat分別指向同一堆積物件。
new Type[N] 在堆積建立固定大小物件。a = b 只是讓 a 指向 b 所指的物件,不改變物件本身。names 陣列),GC 會自動回收。理解引用機制對避免記憶體洩漏、設計高效資料結構、排查 NullPointerException 至關重要。下次看到「陣列大小變了」,請記得:是變數換了指向,不是物件變大了。