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() 裡第二行的時候,記憶體的狀態如下:
接著我們呼叫了 object1.setX(10),這裡要注意的是你會發現雖然 setX(10) 好像只有一個參數,但實際上 Java 會把 object1 內所存放的記憶體地址,也複製一份到 setX() 在 Stack 上被配置到的空間,當成 setX() 的第一個參數:
接著等到執行 x = value 的時候,setX 實際上是透過第一個參數的記憶體位置,去找到目前 object1 所指到的物件,並修改其 x 變數的內容:
接著 object1.setX(10) 返回後,Stack 上 setX() 所屬的區域被消滅:
而接著執行 object2.setX(20) 時也是同樣的流程,但是一開始我們複製的是 main() 裡的 y 所存的記憶體地址:
最後 object2.setX(20) 返回,變成像下面這樣,兩個 Instance Method 分別改到不同的物件,所以印出來會是 10 和 20:
至於其他的部份,其實 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 裡面的記憶體地址」當做第一個參數而已。
回響