你們不要再吵了!Java只有值傳遞..

寫在前邊

上次聊到Java8新特性 lambda時,有小夥伴在評論區提及到了lambda對於區域性變數的引用,補充著部落格的時候,知識點一發散就有了這篇對於

值傳遞還是引用傳遞

的思考。關於這個問題為何會有如此多的

誤區

,這篇就來破解ta!

果然知識網的發散是無止境的!

知識儲備——堆和棧

堆是指動態分配記憶體的一塊區域,一般由程式設計師手動分配,比如 Java 中的 new、c裡邊的malloc。

棧是編譯器幫我們分配好的區域,一般用於存放函式的引數值,區域性變數等

有關堆疊的相關知識在 迷途指標 中有所提及。

資料型別

Java中除了基本資料型別,其他的均是引用型別,包括

陣列

等等。

基本資料型別和引用型別的區別

先看一下這兩個變數的區別

1234void test1(){ int cnt = 0; String str = new String(“melo”);}

你們不要再吵了!Java只有值傳遞..

cnt是基本型別,值就直接儲存在變數中(存放在棧上)

而str是引用型別,變數中儲存的只是實際物件的

地址

。一般稱這種變數為“引用”,引用指向實際物件,實際物件中儲存著

內容

比如我們建立了一個 Student student = new Student(“Melo”);

在堆中開闢一塊記憶體(真正的物件存放在堆上),其中儲存了name等資料 , 而student只是儲存了

該物件的地址(存放在棧上)

當我們修改變數時

123456void test1(){ int cnt = 0; cnt=1; String str = new String(“melo”); str=“Melo”;}

你們不要再吵了!Java只有值傳遞..

對於

基本型別

cnt,賦值運算子會直接改變變數的值,原來的值直接被覆蓋掉了。

ta無依無靠,不像下邊一樣有房子可以住。

對於

引用型別

str,賦值運算子只會改變引用中所儲存的

地址

,雖然原來的地址被覆蓋掉了,str指向了一個新的物件,但是

原來的那個老物件沒有發生變化,他還是老老實實待在原來的地方!!!

有學過c語言的同學應該很清楚,這裡藉助c語言中的“指標”打個比喻。

引用型別str就相當於一個指標(旗子),插在了一個房子門口。現在給這個旗子挪個位置,只是讓這個旗子放置在了另一個新的房子,原本的老房子還在那裡,不會說因為你改變了旗子的位置,房子就塌了。

當然,原來那個房子沒有旗子插著了,沒有人住了。也不能總是放任ta在那佔著空間,過段時間也許就會有人來把他給拆了回收了(JVM)。

這種沒有地方引用到的物件就稱為垃圾物件。

值傳遞

我們上次聊到lambda的時候,提及到了值傳遞,那裡的複製副本,就是我們這裡要說的值傳遞

如果我們這裡的

方法塊

訪問了外部的變數,而這個變數只是一個

普通資料型別

的話,相當於只是訪問到了一份

副本

。當外部對這個變數進行修改時,lambda

內部

(只有副本)是無法感知到這個變數的修改的。

我們只是將實參傳遞給了方法的形參,

將cnt值複製一份,賦值給形參val

所以,函式內對形參的操作完全不會影響到實參真正存活的區域!而伴隨著函式呼叫的結束,形參區域和其內的區域性變數也會被釋放。(方法棧的回收)

12345//基本型別的值傳遞void unChange(int val) { val = 100;}unChange(cnt); // cnt 並沒有被改變

引用傳遞

實參傳遞給形參時,形參其實用的就是實參本身(而不再單純只是複製一份副本出來了),當該形參變數被修改時,實參變數也會

同步修改

Java中到底是引用傳遞還是值傳遞呢

內卷例項

12345678910111213//內卷 void involution(Student temp){ temp。setScore(100); } public static void main(String[] args) { Student student = new Student(); student。setName(“Melo”); student。setScore(0); System。out。println(“躺平時的成績->”+student。getScore()); new TestQuote()。involution(student); System。out。println(“捲了幾天後的成績->”+student。getScore()); }

你們不要再吵了!Java只有值傳遞..

這裡看起來,好像符合我們引用傳遞的定義誒?

對形參temp的修改,會反饋到外部實參student那裡去?看起來操作的是同一個變數的樣子?

反內卷例項

看下邊這段“反內卷”的程式碼例項

1234567891011121314//反內卷 void againInvolution(Student temp){ temp = new Student(); temp。setScore(100); } public static void main(String[] args) { Student student = new Student(); student。setName(“Melo”); student。setScore(0); System。out。println(“企圖內卷前的成績->”+student。getScore()); new TestQuote()。againInvolution(student); System。out。println(“遭受反內卷後的成績->”+student。getScore()); }

你們不要再吵了!Java只有值傳遞..

細心的同學可能發現了,我們這裡多了一步操作 ——>

temp = new Student();

先給出答案吧,Java裡邊其實只有值傳遞!!!

為什麼這麼說?

其實我們這裡的形參temp,只是複製了一份student的地址。可以理解為temp複製了

這條指標

,他也指向了

student所指向的物件。

也就是說,temp只是跟student

同樣

指向了一個

同一個物件

而已,在第一個例子中,我們沒有去重新修改temp的指向,所以會造成一種假象:我們對temp的修改似乎等價於對student的修改? 其實只是剛好兩個指向了同一個物件而已!!

你們不要再吵了!Java只有值傳遞..

而如果我們對temp重新賦值了呢, temp = new Student();

你們不要再吵了!Java只有值傳遞..

對temp重新賦值後,此時temp就指向了另一個區域了,後續再對temp修改,根本不會影響原來的student指向的區域

所以才會“反內卷”失敗,跳出函式的時候,student所指向的物件成績根本沒有增長!!!

為什麼會有誤區呢?

其實還是因為Java中資料型別的問題,基本資料型別看起來就像是值傳遞,而引用傳遞因為存放了地址,讓我們能夠訪問到實參所指向的物件,容易讓我們誤以為我們的形參其實就等價於實參。

其他語言的引用

JS只有值傳遞,類似Java

指標傳遞(C語言)

注意指標傳遞跟引用傳遞是不一樣的

拿最老套的C語言手寫swap來講

1234567891011121314151617#include void swap(int *a, int *b) { int temp; temp = *a; *a = *b; *b = temp;} int main() { int a = 5; int b = 8; //需要傳遞地址 swap(&a, &b); printf(“a = %d\n”, a); printf(“b = %d”, b);}

引用傳遞(C++)

12345678910111213141516171819202122#include using namespace std; int main(){ //&識別符號 void swap(int& x,int& y); int a = 5; int b = 8; swap(a,b); return 0;}void swap(int& a,int& b){ int temp; temp = a; a = b; b = temp;}

總結

如果該語言沒有&,@這種取地址的運算子,

一般

來說就只有值傳遞的。如js和java

經評論區小夥伴補充,不用&,@這種取地址的運算子也可以引用傳遞,參考C#的ref、out和in關鍵字。

而c,Pascal,go這些是可以傳引用和傳值的。

最後

其實關於Java到底是引用傳遞還是值傳遞這個問題。我們只需要理解好本質就好了,透過上邊的那兩幅圖,理解好本質才是關鍵,萬變不離其宗。

本文作者:Melo~

本文連結:https://www。cnblogs。com/melojun/p/15511033。html