Java 也有 Stack / Heap / Pointer 這些東西嗎?
前兩篇文章,我們提到了在 C 語言裡 Stack 與區域變數之間的關係,還有如何用 Pointer 來操作向程式語言 runtime 要回來的 Heap 空間。
你可能會覺得疑惑,這些東西在 Java 當中也有嗎?特別是 Pointer 這東西,大家不是都說 Java 因為沒有 Pointer 所以比 C++ 簡單好學嗎?
事實上,這些東西在 Java 中通通都有,只是 Java 把他隱藏得很好,讓你覺得這些東西好像不存在一樣,但如果你沒有這些觀念,卻會讓你覺得 Java 好像捉摸不定。
Java 變數的分類
如果你是 Java Programmer,應該相當習慣了當你在宣告某些變數的時候,他的型態開頭是小寫:
- byte
- short
- int
- long
- char
- float
- double
- boolean
你也應該習慣了,除了上述八種資料型態外,其他的資料型態應該要用大寫開頭,如果你看到一個變數的宣告是 myType x 這樣,應該立馬會覺得寫出這行程式碼的傢伙對 Java 一定不熟。
這是因為在 Java 裡變數分成兩大類,一種是 Primitive Type,也就是上述列出來的八個小寫開頭的資料型態,而其他所有的資料型態,不論他是啥 Java 類別或介面,一律都算做 Reference 資料型態,而在 Java 的慣例裡,Reference 型態的資料型別習慣上都是大寫開頭。
那 Primitive Type 和 Reference 有什麼不同呢?
簡單的來說如下:
- Primitive Type 的變數裡存的是「值」,例如一個 int x = 10,x 裡存的就是整數 10;一個 double y = 0.5,y 那塊記憶體裡存的就是 0.5。
- Reference 存的是某樣東西的記憶體地址
看到關鍵字了嗎?還記得有什麼東西存的也是記憶體地址嗎?答對了!就是 Pointer!
所以你現在知道為什麼大家說 Java 裡沒有 Pointer 這句話有問題了吧?因為 Java 裡確實有 Pointer,只是他的 Pointer 叫做 Reference,而且你還天天在用他……
最簡單的類別的宣告與物件配置
我們已經知道,在 Java 裡,你宣告在方法當中的變數,都是存在 Stack 上了,所以 Stack 在 Java 當中確實是有被用到沒有錯,但 Heap 呢?我從來沒在 Java 裡向 JVM 要 Heap 裡的空間的樣子啊?
沒錯,因為 Java 並不像 C 一樣允許你直接操作記憶體,但就某方面來說,你寫 Java 的時候用到 Heap 的機會比 C 還大很多--因為所有你 new 出來的 Java 物件全部都長在 Heap 上。
多說無益,我們直接來看程式碼吧:
public class SimpleObject
{
private int x = 10;
public static void main (String [] args)
{
SimpleObject object1;
SimpleObject object2;
object1 = new SimpleObject();
object2 = new SimpleObject();
object1.x = 20;
System.out.println(object1.x);
System.out.println(object2.x);
}
}
這個程式非常簡單,我們定義了一個 SimpleObject 的類別,並且在 main() 裡建立了兩個 SimpleObject 物件,然後把 object1 物件的整數變數 x 設成 20。
這個時候的記憶體裡究竟發生了哪些事呢?這個時候你要試著回答:
- object1 和 object2 的變數放在記憶體的哪裡?
- 我們 new 出來的物件又放在記憶體的哪裡?
- 這兩個物件中的 x 變數又是在哪裡呢?
答案如下:
- object1 和 object2 都放在 Stack 上給 main() 函式的空間
- 這兩個物件都在 Heap 裡
- 因為 x 是成員變數,所以是跟著物件,既然物件在 Heap 裡,他們也應該會在 Heap 裡。
另外你也應該注意到了,你在寫 Java 的時候只有對於 Reference 型態的變數才會加上 . 後面接著某些東西,這個 . 就類似 C 的 Pointer 前加上 * 號做 deference 來取得 Pointer 指到的位置一樣。
換句話說,你也可以把 object1.x 記成像「找出 object1 裡存的地址所指到的 Heap 空間當中的 x」這樣的意思。
有了上述的概念之後,我們就可以試著把這個程式在執行時的記憶體狀態一步一步畫出來……
現在你應該很清楚自己在 Java 裡 new 一個物件出來的時候,會發什生麼事情了吧?
那再多一點成員變數呢?
你可能會問,在上面的例子裡 SimpleObject 只有一個 x 變數,如果我再加一個 y 變數上去,會變怎麼樣呢?其實也沒怎樣--就是在 Heap 上的物件大了一點,多給你一點空間放 y 變數而已。
例如下面的程式:
public class SimpleObject
{
private int x = 10;
private double y = 20.0;
public static void main (String [] args)
{
SimpleObject object1;
SimpleObject object2;
object1 = new SimpleObject();
object2 = new SimpleObject();
object1.x = 20;
System.out.println(object1.x);
System.out.println(object2.x);
}
}
在執行完 object1.x = 20 這行之後,記憶體的狀況會如下圖所示:
那如果成員變數裡也有 Reference 型態呢?
上面的兩個程式碼裡,我們的 SimpleObject 類別的成員變數都只有 int 和 double 這些以小寫開頭的基本資料型態,那如果我們的類別比較複雜,不只有這些數值的資料,還有其他物件呢?
以下面的程式碼來看,我們的記憶體到底會變得怎麼樣呢?
class AnotherObject
{
private int x = 30;
}
public class SimpleObject
{
private int x = 10;
private double y = 20.0;
private AnotherObject z = new AnotherObject();
public static void main (String [] args)
{
SimpleObject object1;
SimpleObject object2;
object1 = new SimpleObject();
object2 = new SimpleObject();
object1.x = 20;
System.out.println(object1.x);
System.out.println(object2.x);
}
}
只要你記得以下兩點:
- 所有 Java 物件都在 Heap 上
- 所有非基本資料型態的變數,都是 Reference,存的都是地址
那你應該很容易得出以下的結論:
- 現在 Heap 上有四個物件:
- object1 指到的 SimpleObject
- object2 指到的 SimpleObject
- object1 指到的 SimpleObject 裡的 z 指到的 AnotherObject
- object2 指到的 SimpleObject 裡的 z 指到的 AnotherObject
- 既然 object1 和 object2 裡面的 z 都是 Reference,他們存的一定是記憶體地址!
- 所以 object1.z 存的是上述的第 3 個物件
- 而 object2.z 存的是上述的第四個物件
有了這些線索,我們就可以很簡單地把記憶體當中的情況畫出來了:
小結
這一次我們講的是關於 Java 中物件和 Heap 的關係,你可以看到 Java 是如何存放我們所 new 出來的物件,和他與區域變數之間的關係。
這一節的重點如下:
- Java 的資料型態分成 primitive type 和 reference type 兩種,其中 reference type 其實就是指標,存的是記憶體地址。
- Java 裡所有的物件都是存在 Heap 上面。
到了這邊,你應該可以了解其實當你 new 一個 Java 物件的時候,其實就和 C 語言的 malloc 一樣,是在向程式語言的 runtime(這裡的話就是 JVM)要 Heap 的空間。
可是你可能也注意到了,你在寫 Java 程式的時候,從來沒有寫過「把記憶體還給 JVM」這樣的程式碼,頂多是把某個 Reference 變數設為 null 而已,下次我們會詳細來看為什麼在寫 Java 時,不需要明確地把從 Heap 挖來的空間歸還給 JVM。
回響