Java 中只有按值傳遞
"Java 中只有按值傳遞",作為初學者初看到這幾個字有點不敢相信,無數次通過函式改變過物件,無數次跟同事說 Java 在傳物件的時候是按引用傳遞。後來細細想想,之所以以為 Java 傳物件是按引用傳遞是因為其中有很多概念都沒理清楚,與 C++ 中的搞混了。從 C++ 轉 Java 的時候將 C++ 中的知識點對映到 Java 沒錯,這有利於 C++ 轉 Java 的人更快的學習 Java。但一旦對映錯誤就很容易形成固定思維。
在 C++ 和 Java 中都有引用的概念,但他們完全不是同一個東西。Java 中的引用更類似 C++ 的指標,C++ 的引用在 Java 中並無對應概念。在 C++ 中有按值傳遞、按指標傳遞和按引用傳遞三種,而在 Java 中沒有 C++ 引用和指標的概念,只保留了按值傳遞。
為了更好的說明 Java 中只有按值傳遞,先來看看 Java 的資料型別,Java 的資料型別分為基本資料型別和引用型別,其中:
-
基本型別包括 byte/short/int/long/float/double/char/boolean 八種,基本型別在記憶體中地址中儲存的即本身的值,其一般都在棧上分配。
-
引用型別指向一個物件,它與 C++ 的指標非常相似。但 C++ 的指標可以指向基本型別和類物件,而 Java 的引用只能指向類(列舉、介面等)物件。Java 中物件本身在堆上分配,而引用型別在棧上分配,其記憶體地址中儲存的是物件在堆中的地址。兩種型別在記憶體中的佈局如下:
1 class MyInteger {
2 int value;
3 }
4
5 public class TestReference {
6
7 public static void changeBasic(int arg) {
8 arg = 2;
9 }
10
11 public static void changeReference(MyInteger arg) {
12 arg.value = 2;
13 }
14
15 public static void main(String[] args) {
16
17 int basicTypeA = 1;
18
19 MyInteger referenceTypeA = new MyInteger();
20 referenceTypeA.value = 1;
21
22 System.out.println("呼叫 changeBasic 之前 basicTypeA 的值 "+ basicTypeA);
23 changeBasic( basicTypeA);
24 System.out.println("呼叫 changeBasic 之後 basicTypeA 的值 "+ basicTypeA);
25
26 System.out.println("呼叫 changeReference 之前 referenceTypeA 的值 "+ referenceTypeA.value);
27 changeReference( referenceTypeA);
28 System.out.println("呼叫 changeReference 之後 referenceTypeA 的值 "+ referenceTypeA.value);
29 }
30 }
複製程式碼
執行結果如下:
可以看出基本型別 int 的變數 basicTypeA 在 changeBasic 呼叫後值並沒有發生改變,而引用型別 MyInteger 的變數 referenceTypeA 在呼叫 changeReference 後發生了改變。這裡就比較容易誤導讀者以為:Java 基本型別是按值傳遞而引用型別是按引用傳遞(暫且這麼定義)。其實不然,按值傳遞的意思想必大家都知道:傳遞的是值的的拷貝,比如上面程式碼中的呼叫 changeBasic(basicTypeA) 時,arg 是 basicTypeA 的一個拷貝,所以無論對 arg 做任何操作都不影響 basicTypeA 變數本身。而呼叫 changeReference(referenceTypeA) ,arg 也是 referenceTypeA 的一個拷貝,但是由於 arg 和 referenceTypeA 都是引用型別且他們指向同一物件,所以通過 arg 修改物件,referenceTypeA 也能看到。兩種型別變數在記憶體中呼叫過程如下:
所以可以看出無論是基本型別還是引用型別,都是按值傳遞。只是由於它們在記憶體中所表示的內容不同,最後表現出來的結果也有所不同。同理,在 C++ 中的按值傳遞、按指標傳遞和按引用傳遞理論上都可以歸為按值傳遞(其實這個歸類在學 C++ 的時候就歸納出來了,只是後來反而忘了)。
對"引用"進行按值傳遞的坑
Java 的引用類似於 C++ 的指標,但是 C++ 的物件(不包括基本型別)傳遞提供了物件本身直接傳遞和指標傳遞兩種方式(引用方式不談),而 Java 物件只有對引用進行傳遞這一種,不存在直接將物件本身進行傳遞。
- 物件本身進行傳遞的好處是傳遞的都是物件的拷貝,在函式中對拷貝的物件做任何修改都不會改變原物件。但是如果傳遞物件非常大,而且呼叫很頻繁會影響效能。
- 物件的引用(或者指標)傳遞的好處是隻需要拷貝一個引用(或者指標)大小的資料即可,且可以在呼叫的函式中改變原物件內容。缺點是容易挖坑。
在 C++ 中以上兩種傳遞方式可以自行選擇,而 Java 裡面只有第 2 種方式,凡事有利有弊,有時候我們並不想在函式中改變原物件的內容,這裡我就踩過一個坑,我們的專案中有個物件通過管道傳遞的流程如下:
funcA 與 funcB 是兩個不同的人負責的,一次升級後 funcB 的負責人發現在函式中獲取的物件 X 內容不對,一開始還以為是傳遞物件 X 的介面出現了錯誤便是一頓排查,知道最後才發現物件 X 是在升級後在 funcA 中被修改了,浪費了不少時間。當然這個架構的流程設計的不合理是主要原因(只需要在分發的時候講物件 X 做手動拷貝即可避免上述問題),但是不不影響我們丟擲 Java 只能對引用進行傳遞的弊端。
在呼叫鏈較長、各種 for/while 迴圈中很容易就犯了上述錯誤,解決方案當然就是手動拷貝物件,Java 中拷貝物件有以下兩種方式:
- 實現Cloneable介面並重寫Object類中的clone()方法。
- 實現Serializable介面,通過物件的序列化和反序列化實現克隆,可以實現真正的深度克隆。
其中第二種方式能避免深淺拷貝的問題,但呼叫比較耗時。第一種也能避免深淺拷貝但是需要自己手動去寫相應的程式碼,如果巢狀較深,程式碼將非常複雜。至於深淺拷貝的問題可以自行百度,其本質還是因為只是將物件的引用進行了傳遞而導致的一些問題。