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

<< 上一篇

Java 裡的 Non-Static 方法

上一篇我們討論了在 Java 裡函式的參數傳遞行為,也提到了在 Java 裡只有 Call By Value 這一種參數傳遞的方式,而你能傳遞的就只有八種 primitive type 的資料,再加上記憶體地址這九種東西而已。

不過在前一章中,我們討論的也僅止於 static 方法,完全沒提到當呼叫 Instance Method 的時候會發生什麼事情,這次我們就來看一下這個問題。

首先來看一下下面的程式碼:

public class MyObject
{
    int x = 0;

    void setX(int value)
    {
        x = value;
    }

    public static void main(String [] args)
    {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();

        object1.setX(10);
        object2.setX(20);

        System.out.println(object1.x);
        System.out.println(object2.x);
    }
}

熟悉 Java 的朋友應該都知道,上面的程式碼印出來的結果會是第一行是 10,第二行是 20。

一般 Java 教學書上的解釋會是說,Instance Method 所改變到的,會是接收者物件的狀態,而在我們上述的例子中,因為兩個 setX 的接收者不同,所以會改到不同物件的狀態。

但你有沒有好奇過,為什麼明明都是一樣的 x = value 程式碼,Java 卻可以區分出來到底要去改哪個物件的 x 呢?

呼叫 Instance Method 時 Java 幫你加的料

上面那個問題的答案其實很簡單,因為當你在呼叫 Instance Method 的時候,Java 自動幫你在你傳給 Method 的參數裡面加料了。

當你寫下 object1.setX(20) 的時候,事實上 Java 是丟了 (object1, 20) 這樣的參數到 setX 的 Call Stack 上的區域的,而當你在 setX 要找原本物件裡面的變數時,就會跑去第一個參數找。

換句話說,當你在執行上面的程式的時候,執行到 main() 裡第二行的時候,記憶體的狀態如下:

Stack Status

接著我們呼叫了 object1.setX(10),這裡要注意的是你會發現雖然 setX(10) 好像只有一個參數,但實際上 Java 會把 object1 內所存放的記憶體地址,也複製一份到 setX() 在 Stack 上被配置到的空間,當成 setX() 的第一個參數:

Stack Status

接著等到執行 x = value 的時候,setX 實際上是透過第一個參數的記憶體位置,去找到目前 object1 所指到的物件,並修改其 x 變數的內容:

Stack Status

接著 object1.setX(10) 返回後,Stack 上 setX() 所屬的區域被消滅:

Stack Status

而接著執行 object2.setX(20) 時也是同樣的流程,但是一開始我們複製的是 main() 裡的 y 所存的記憶體地址:

Stack Status
Stack Status

最後 object2.setX(20) 返回,變成像下面這樣,兩個 Instance Method 分別改到不同的物件,所以印出來會是 10 和 20:

Stack Status

至於其他的部份,其實 non-static 方法和 static 方法,並沒有什麼太大的差別,同樣都是使用 Call By Value 的方式傳遞那八個 primitive type 和記憶體地址而已。

為什麼不能在 Static Method 裡呼叫 Non-Static Method

有的時候我們在寫一些比較小的 Java 作業(例如解 Project Euler 上面的題目)時,可能會不需要用到物件導向的功能,所以會把所有的 method 都宣告成 static,把 Java 當 C 這類的 Procedure Language 來用,而在這個時候我們有可能會把 static 關鍵字遺忘掉,變成像下面這樣:

public class Test
{
    public void nonStatic() { System.out.println("Hello World"); }
    public static void main(String [] args)
    {
        nonStatic();
    }
}

在這個時候,Java 的編譯器就會告訴你:

brianhsu@USBGentoo ~ $ javac Test.java
Test.java:10: non-static method nonStatic() cannot be referenced from a static context
        nonStatic();
        ^
1 error

現在你應該了解 Java 會告訴你不能在 static 方法裡呼叫 non-static 方法了吧?因為在這個情況下,你根本沒有「目前的物件的 Reference」的這個東西,可以當做 nonStatic() 的第一個參數,所以當然無法呼叫 nonStatic() 這個方法囉。

還是 AutoBoxing 的問題

Java 的函式呼叫到這裡大致上已經差不多了,不過還是要提醒一下,在函式呼叫的時候,Java 的 AutoBoxing 機制也一樣會運作,例如下面的程式碼:

public class Test
{
    public static void eatAnything(Object obj)
    {
        System.out.println("I eat " + obj);
    }

    public static void main(String [] args)
    {
        int primitiveInt = 12345;
        eatAnything(primitiveInt);
    }
}

雖然你預期 eatAnything 接收的是指到 Object 類型的物件的 Reference,但由於 Java 具有 AutoBoxing 的機制,所以就算你丟進去的是一個 primitive 的整數,Java 編譯還是會讓你過關,eatAnything 還是會把整數吃下去。

只是在這個時候,eatAnything 吃下去的實際上已經是 Java 自動幫你建立的 Integer 物件的 Reference,而不是原本的 primitive type 的整數了。

public class Test
{
    public static void eatAnything(Object obj)
    {
        System.out.println("I eat " + obj);
    }

    public static void main(String [] args)
    {
        int primitiveInt = 12345;
        eatAnything(new Integer(primitiveInt));
    }
}

這件事看起來好像沒有什麼,但事實上物件的建立比起單純 primitive type 的「複製數值」的動作還是慢上一截,再加上還有 GC 回收你所建立的物件所耗的資源與時間,所以如果你是在寫比較講求執行速度的程式的話,能夠必免用 Boxing 的方式傳 primitive type 的話,就盡量避免吧。

小結

這一篇裡面我們談的主要是 non-static 方法的參數傳遞,你會發現他和一般的 static 方法並沒有什麼太大的不同,都還是使用 Call By Value 的方式來運作,只是 Java 多幫你傳了一個「指到目前的物件的 Reference 裡面的記憶體地址」當做第一個參數而已。

下一篇 >>

回響