Java中的參數傳遞,到底是值傳遞還是引用傳遞?

文章內容爲轉載,轉載自公衆號Hollis關於我要徹底給你講清楚,Java就是值傳遞,不接受爭辯的那種!

Java中的參數傳遞,到底是值傳遞還是引用傳遞?

結論:Java只有值傳遞,沒有引用傳遞!

錯誤理解一:值傳遞和引用傳遞,區分的條件是傳遞的內容,如果是個值,就是值傳遞。如果是個引用,就是引用傳遞。

錯誤理解二:Java是引用傳遞。

錯誤理解三:傳遞的參數如果是普通類型,那就是值傳遞,如果是對象,那就是引用傳遞。

實參與形參

我們都知道,在Java中定義方法的時候是可以定義參數的。比如ava中的main方法,public static void main(String[] args),這裏面的args就是參數。參數在程序語言中分爲形式參數和實際參數。

形式參數:是在定義函數名和函數體的時候使用的參數,目的是用來接收調用該函數時傳入的參數。

實際參數:在調用有參函數時,主調函數和被調函數之間有數據傳遞關係。在主調函數中調用一個函數時,函數名後面括號中的參數稱爲“實際參數”。

簡單舉個例子:


實際參數是調用有參方法的時候真正傳遞的內容,而形式參數是用於接收實參內容的參數。

基本類型與引用類型

int num = 10;
String str = "hello";


如圖所示,num 是基本類型,值就直接保存在變量中。而 str是引用類型,變量中保存的只是實際對象的地址。一般稱這種變量爲 “引用”,引用指向實際對象,實際對象中保存着內容。

賦值運算符“=”的作用

num = 20;
str = "java";

對於基本類型 num,賦值運算符會直接改變變量的值,原來的值被覆蓋掉。

對於引用類型 str,賦值運算符會改變引用中所保存的地址,原來的地址被覆蓋掉。但是原來的對象不會被改變(重要)。

如上圖所示,“hello” 字符串對象沒有被改變。(沒有被任何引用所指向的對象是垃圾,會被垃圾回收器GC回收

值傳遞與引用傳遞

上面提到了,當我們調用一個有參函數的時候,會把實際參數傳遞給形式參數。但是,在程序語言中,這個傳遞過程中傳遞的兩種情況,即值傳遞和引用傳遞。我們來看下程序語言中是如何定義和區分值傳遞和引用傳遞的

值傳遞(pass by value)是指在調用函數時將實際參數複製一份傳遞到函數中,這樣在函數中如果對參數進行修改,將不會影響到實際參數。

引用傳遞(pass by reference)是指在調用函數時將實際參數的地址直接傳遞到函數中,那麼在函數中對參數所進行的修改,將影響到實際參數。

  • 值傳遞:將參數複製一份,修改形參不會對實參造成影響
  • 引用傳遞:將實參的地址傳遞給形參,修改形參也就是在修改實參

我們來測試幾段代碼:

上面的代碼中,我們在 pass 方法中修改了參數 j 的值,然後分別在 pass 方法和 main 方法中打印參數的值。輸出結果如下:

print in pass , j is 20

print in main , i is 10

可見,pass 方法內部對 name 的值的修改並沒有改變實際參數 i 的值。那麼,按照上面的定義,有人得到結論:Java 的方法傳遞是值傳遞。

但是,很快就有人提出質疑了(哈哈,所以,不要輕易下結論咯。)。然後,他們會給出以下代碼:

同樣是一個 pass 方法,同樣是在 pass 方法內修改參數的值。輸出結果如下:

print in pass , User{name='Tom', sex='man'}
print in main , User{name='Tom', sex='man'}

經過 pass 方法執行後,實參的值竟然被改變了,那按照上面的引用傳遞的定義,實際參數的值被改變了,這不就是引用傳遞了麼。於是,根據上面的兩段代碼,有人得出一個新的結論:Java 的方法中,在傳遞普通類型的時候是值傳遞,在傳遞對象類型的時候是引用傳遞。

但是,這種表述仍然是錯誤的。不信你看下面這個參數類型爲引用類型的參數傳遞:

print in pass , Tom
print in main , Mr.Q

那麼,問題來了。String是引用類型,new String("Mr.Q")在堆上創建了對象,name指向了Mr.Q的引用。那按照上面來說,應該是引用傳遞了,輸出的結果應該pass和main是相同的,可是,爲什麼會不同呢?

這又作何解釋呢?同樣傳遞了一個對象,但是原始參數的值並沒有被修改,難道傳遞對象又變成值傳遞了?

其實,是傳遞的地址值發生了改變

String類型在值傳遞和引用傳遞問題中比較特殊,爲什麼說特殊呢,因爲對於一些常量字符串的創建,只要判斷對象在堆中不存在,便會創建一個新的,如果是創建新對象,那麼引用地址都會變。我們可以通過一個簡單的例子來解釋下:

a是:hello --- b是:你好

String a = “hello”; 在 String 池中檢查並創建一個常量:“hello”,給 a 分配一個棧內存,在此存儲常量 hello 的地址。

String b= a; 給 b 分配一個棧內存,在此存儲常量 hello 的地址。相當於 a 把自己持有的地址,複製給了 b。

b = “你好”; 在 String 池中檢查是否有 “你好” 的常量。

  • 如果有,將 b 的地址指向 “你好” 的地址。
  • 如果 String 池中沒有 “你好” 常量,在堆內存中創建 “你好” 常量,並將 b 地址指向 “你好”。

Java中的值傳遞

值傳遞和引用傳遞之前的區別到底是什麼?

兩者的最主要區別就是是直接傳遞的,還是傳遞的是一個副本

這裏我們來舉一個形象的例子。再來深入理解一下傳值調用和傳引用調用:


  • 你有一把鑰匙,當你的朋友想要去你家的時候,如果你直接把你的鑰匙給他了,這就是引用傳遞。
  • 這種情況下,如果他對這把鑰匙做了什麼事情,比如他在鑰匙上刻下了自己名字,那麼這把鑰匙還給你的時候,你自己的鑰匙上也會多出他刻的名字。

  • 你有一把鑰匙,當你的朋友想要去你家的時候,你復刻了一把新鑰匙給他,自己的還在自己手裏,這就是值傳遞。
  • 這種情況下,他對這把鑰匙做什麼都不會影響你手裏的這把鑰匙。

那我們再說回到這段代碼中:

print in pass , User{name='Tom', sex='man'}
print in main , User{name='Tom', sex='man'}

看看在調用中,到底發生了什麼?

在參數傳遞的過程中,實際參數的地址0x666拷貝給了形參。這個過程其實就是值傳遞(這個值,理解爲引用的地址),只不過傳遞的值得內容是對象的應用。

那爲什麼我們改了user中的屬性的值,卻對原來的user產生了影響呢?

其實,這個過程就好像是:你複製了一把你家裏的鑰匙給到你的朋友,他拿到鑰匙以後,並沒有在這把鑰匙上做任何改動,而是通過鑰匙打開了你家裏的房門,進到屋裏,把你家的電視給砸了。

這個過程,對你手裏的鑰匙來說,是沒有影響的,但是你的鑰匙對應的房子裏面的內容卻是被人改動了。

也就是說,Java對象的傳遞,是通過複製的方式把引用關係傳遞了,如果我們沒有改引用關係,而是找到引用的地址,把裏面的內容改了,是會對調用方有影響的,因爲大家指向的是同一個共享對象。

那麼,如果我們改動一下pass方法的內容:

上面的代碼中,我們在pass方法中,重新new了一個user對象,並改變了他的值,輸出結果如下:

print in pass , User{name='Tom'}
print in main , User{name='Mr.Q'}

也就是說,我把我的鑰匙複製給了我的朋友,但是我立馬換了我家的鎖。因爲一new就會在堆上開闢新空間,地址就發生了改變,此時的user不再指向0x666了,理解爲我換鎖了,朋友當然進不了我家,砸不了電視了。所以此時在pass方法中修改name,不會對我家造成任何影響。

上面這種傳遞是什麼傳遞?肯定不是引用傳遞,如果是引用傳遞的話,在user = new User()的時候,實際參數的引用也應該改爲指向0x999,但是實際上並沒有。

通過概念我們也能知道,這裏是把實際參數的引用的地址複製了一份,傳遞給了形式參數。所以,上面的參數其實是值傳遞,把實參對象引用的地址當做值傳遞給了形式參數。


所以,值傳遞和引用傳遞的區別並不是傳遞的內容,而是實參到底有沒有被複制一份給形參

在判斷實參內容有沒有受影響的時候,要看傳的的是什麼,如果你傳遞的是個地址,那麼就看這個地址的變化會不會有影響,而不是看地址指向的對象的變化。

所以說,Java 中其實還是值傳遞,只不過對於對象參數,值的內容是對象的引用。


總結

無論是值傳遞還是引用傳遞,其實都是一種求值策略 (Evaluation strategy)。在求值策略中,還有一種叫做按共享傳遞 (call by sharing)。其實 Java 中的參數傳遞嚴格意義上說應該是按共享傳遞。

按共享傳遞,是指在調用函數時,傳遞給函數的是實參的地址的拷貝(如果實參在棧中,則直接拷貝該值)。

在函數內部對參數進行操作時,需要先拷貝的地址尋找到具體的值,再進行操作。

如果該值在棧中,那麼因爲是直接拷貝的值,所以函數內部對參數進行操作不會對外部變量產生影響。

如果原來拷貝的是原值在堆中的地址,那麼需要先根據該地址找到堆中對應的位置,再進行操作。因爲傳遞的是地址的拷貝,所以函數內對值的操作對外部變量是可見的。

簡單點說,Java 中的傳遞,是值傳遞,而這個值,實際上是對象的引用。

  • 傳遞的值在棧中,直接拷貝一份值傳遞,改變的形參不會對實參造成影響
  • 傳遞的值在棧中存放的是地址(引用),先根據棧中的地址找到在堆上的值,然後把地址拷貝一份(拷貝的地址是一個值),此時形參和實參指向堆上同一個地址,形參的修改導致了實參的改變。

Java中的參數傳遞,是值傳遞!

Java只有值傳遞!

Java只有值傳遞!

Java只有值傳遞!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章