淺談 C 語言和 Java 的記憶體管理(八)

<< 上一篇

說到 C / Java 中的 == 這東西

我們在寫程式的時候,經常會需要比較兩樣東西是否相同,藉此來決定程式的執行流程,而在 C 和 Java 裡面,要比較兩個東西是否相等的時候,通常會使用 == 這個運算子。

在大部份的情況下,== 產生的結果都會和我們預期的相同,例如以下的例子裡,不論在 C 或 Java 裡,得出的結果都會是「真」(C 語言裡 0 代表 False,非零值代表 True)。

int x1 = 3;
int x2 = 3;

// printf("%d", x1 == x2);  // C 語言會印出 1
// System.out.println(x1 == x2); // Java 會印出 true

但如果你從來沒寫過 C 或 Java,而是從 Python / Ruby / PHP 這類程式入門的話,可能會覺得 == 這個運算子在某些情況下的行為很詭異,例如如果你要在 C 和 Java 裡的比較兩個字串是否相同的話,像下面的例子一樣用 == 是行不通的。

#include <stdio.h>

int main()
{
    char str1 [] = "Hello World";
    char str2 [] = "Hello World";

    char * str3 = "Hello World";
    char * str4 = "Hello World";

    printf("%d\n", str1 == str2);   // 印出 0,代表 False
    printf("%d\n", str3 == str4);   // 印出 1,代表 True
}
public class Test
{
    public static void main (String [] args)
    {
        String str1 = new String("Hello World");
        String str2 = new String("Hello World");
        String str3 = "Hello World";
        String str4 = "Hello World";

        System.out.println(str1 == str2);   // 印出 false
        System.out.println(str3 == str4);   // 印出 true
    }
}

執行上面的程式的話,你會發現 == 這個運算子比較的明明都是 "Hello World" 這個字串,但他有的時候會出現 true,有的時候卻又出現 false,這是為什麼呢?

原因很簡單--你誤解了 == 這個符號的意義,他比較的不是字串!在 C / Java 裡面,== 這個符號比的是變數的「值」!

C 語言裡的字串和 == 的關係

要了解上面的 C 語言程式碼裡,為什麼會出現這樣的行為,需要先理解在 C 語言裡當我們寫下了 "Hello World" 這種我們習慣稱為「字串」的東西的時候,他到底代表了什麼。

其實很簡單--他是一個以 \0 這個字元(也就是該字元所佔的位元都零的字元,通稱 NULL Character)結尾的字元陣列,也就是說,嚴格來說,C 語言裡的沒有「字串」這種資料型態,有的只是以 NULL Character 結尾的陣列而已,"Hello World" 實際上就是 {'H', 'e', 'l', 'l', o', ' ', 'W', 'o', 'r', 'l', 'd', '\0'} 這樣的字元陣列而已。

這也是為什麼我們會在上面看到,在上面的程式碼裡,我們的 str1 和 str2 宣告的會是一個 char [],也就是說,str1 和 str2 實際上是一個陣列,而我們已經知道在 C 語言裡這樣宣告的話,該陣列的內容會直接放在 Stack 上面。

另一方面,當我們寫下 char * str3 = "Hello World";char * str4 = "Hello World"; 的時候,又發生了什麼事呢?

是這樣的,C 語言編譯器看到這樣的敘述時,會把 "Hello World" 這個字串,放到記憶體裡存放常數的區塊當中,而因為常數是不可以更動的,大部份的編譯器都夠聰明,知道們沒必要存放兩份同樣的資料在常數區中。

所以實際上的情況,會變成 str3 和 str4 裡,存的會是常數區裡 "Hello World" 所在的記憶體位置,也就是說 str3 和 str4 這兩個 Pointer 指到了相同的地方。

換而言之,如果我們把他們畫出來,會變成像下面這個樣子:

Stack Status

有了上面的圖,我們就可以很清楚實際上 str1 == str2str3 == str4 比的到底是什麼了。

我們已經知道,str1str2 是陣列,而當我們直接寫 str1str2 的時候,實際上會被 evaluate 成為這兩個陣列的開頭的記憶體地址,而 str1str2 是兩個獨立的陣列,其開頭位址自然不同,所以 str1 == str2 比較出來的結果會是 false。

另一方面,由於 str3str4 這兩變數是 Pointer 型態,我們直接寫下 str3str4 的時候,就是直接取出這兩個變數所存放的值,而 Pointer 的值就是「記憶體地址」,再加上 str3str4 存放的記憶體地址都是相同的(都指到常數區的 Hello World 的開頭),所以用 == 算出來的當然就是 true 囉。

這也是為什麼平平都是 "Hello World" 這個字串,但比較出來的結果卻完全不同的原因。所以下次要在 C 語言裡比較字串,記得不要再用 == 來比了,因為他做的事情和你想的事情可能完全不一樣。

在 C 裡要比字串的「內容」是不是相同的話,請使用 strncmp()strcmp() 或其他的 C 語裡函式庫其他比較記憶內容的函式。

Java 裡的字串和 == 的關係

相同的,在上面的 Java 程式中,== 所做的事情,也不是比較「字串」。

還記得嗎?我們曾經說過,在 Java 裡資料型態分為 primitive 和 reference 這兩大類,而習慣上 Reference 類型的資料型態都會以大寫開頭。

而在 Java 裡面,字串的型別是 String,也就是說在 Java 裡字串是放在 Heap 上的物件,而你操作他的方式是使用 Reference 來操作。

有了這樣的概念之後,我們再回過頭來看下面的 Java 程式碼:

public class Test
{
    public static void main (String [] args)
    {
        String str1 = new String("Hello World");
        String str2 = new String("Hello World");
        String str3 = "Hello World";
        String str4 = "Hello World";

        System.out.println(str1 == str2);   // 印出 false
        System.out.println(str3 == str4);   // 印出 true
    }
}

套用至今為止我們所知道的東西,我們可以理解到 str1 和 str2 這兩個變數是放在 Stack 上,而他們存的是記憶體地址,分別指到 Heap 上我們所 new 出來的兩個 "Hello World" 字串物件上。

至於 str3 和 str4,我們已經可以看到他們的資料型別是 String,也就是他們和 str1 還有 str2 同樣都是存放記憶體地址,而順著這個記憶體地址,可以找到一個 String 物件。

所以在上面的程式碼中,str1 == str2 會印出 false,因為 str1 首 str2 所存的「記憶體地址」是不同的,他們分別指到了兩個不一樣的 String 物件,而在這個時候 == 比的是 str1 和 str2 裡存的「記憶體地址」,既然兩個變數的記憶體地址不一樣,那當然會是 false 囉。

那為什麼 str3 == str4 又會印出 true 呢?答案是 Java 的編譯器看到那兩句時,會知道 "Hello World" 是個常數,所以會把他放到常數區塊,而既然是常數,就不需要放兩份佔空間,只要把 str3 和 str4 都指到這個常數的 "Hello World" 就好了。

換句話說,這個程式的記憶體狀態長成這樣:

Stack Status

所以在 Java 裡面,當你要比較的是兩個字串長得一不一樣時,不應該去用 == 這個運算子,而是使用該字串物件的 equals 方法,例如 str1.eqauls(str2) 這樣,這個時候雖然 str1 和 str2 分別指到的是不同的字串物件,但 equals 方法會去看這兩個字串物件的「內容」是不是一樣,而由於 str1 和 str2 這兩個字串物件的內容都是 "Hello World",所以就會回傳 true。

public class Test
{
    public static void main (String [] args)
    {
        String str1 = new String("Hello World");
        String str2 = new String("Hello World");
        String str3 = "Hello World";
        String str4 = "Hello World";

        System.out.println(str1.equals(str2));   // 印出 true
        System.out.println(str3.equals(str4));   // 印出 true
    }
}

為什麼大家說 Java 的 String 是不可更動的?

當第一次從 C 轉到 Java 時,經常會看到書上或聽到大家說 Java 裡的字串是不可更動的,但這個時候 Java 初學常常會感到疑惑,因為我在寫程式的時候,明明就可以更改字串啊,像下面這個程式碼:

public class Test
{
    public static void main (String [] args)
    {
        String str1 = "Hello World";
        System.out.println("str1:" + str1);

        str1 = "Another";
        System.out.println("str1:" + str1);

        str1 = str.replace("A", "B");
        System.out.println("str1:" + str1);
    }
}

你看,我先把 "Hello World" 字串改成了 "Another",然後又把 "Another" 這個字串改成了 "Bnother",Java 裡的字串明明就是可以變的啊!

事實上這是個陷阱,在上面的程式中,實際上會有三個獨立的 String 物件,而當這些 String 物件建立之後,他們的內容就是固定的,而不會變改變,而像 str1.replace() 這種看似會改變字串內容的方法,實際上都是建立另一個新的 String 物件,然後返回那個新 String 物件的 reference 給你。

所以實際上上面的程式碼執行的記憶體狀態如下:

Stack Status
Stack Status
Stack Status

在上面的圖裡面,我們可以看到,在程式執行當中所改動到的,只有左邊 Stack 上 str1 中儲存的記憶體地址而已,右邊的字串物在建立之後,其內容是不會變改變的,這也是所謂「Java 的字串是不可更動 (immutable)」的意思--我們雖然可以改動 str1 指到哪個字串物件,但我們無法改動 str 指到的字串物件本身的內容。

小結

這一次我們看了 C / Java 裡的 == 這個運算符號在比較的到底是什麼東西,而我們也知道了在 Java 裡如果要比較兩個字串是不是相同的話,應該要使用 equals 這個方法才對。

而實際上,所有的 Java 物件都會 equals()hashCode() 這兩個方法,而這兩個方法在我們把 Java 物件丟到像是 ArrayList 或 Set 等容器物件中時,會有很重要影響,我們下次就會來仔細看一下 equals()hashCode() 到底是啥。

下一篇 >>

回響