寫在前邊
- 上次聊到Java8新特性 lambda時,有小夥伴在評論區提及到了lambda對於區域性變數的引用,補充著部落格的時候,知識點一發散就有了這篇對於值傳遞還是引用傳遞的思考。關於這個問題為何會有如此多的誤區,這篇就來破解ta!
果然知識網的發散是無止境的!
知識儲備--堆和棧
- 堆是指動態分配記憶體的一塊區域,一般由程式設計師手動分配,比如 Java 中的 new、c裡邊的malloc。
- 棧是編譯器幫我們分配好的區域,一般用於存放函式的引數值,區域性變數等
有關堆疊的相關知識在 迷途指標 中有所提及。
資料型別
Java中除了基本資料型別,其他的均是引用型別,包括類、陣列等等。
基本資料型別和引用型別的區別
先看一下這兩個變數的區別
void test1(){
int cnt = 0;
String str = new String("melo");
}
- cnt是基本型別,值就直接儲存在變數中(存放在棧上)
- 而str是引用型別,變數中儲存的只是實際物件的地址。一般稱這種變數為"引用",引用指向實際物件,實際物件中儲存著內容。
比如我們建立了一個 Student student = new Student("Melo");
- 在堆中開闢一塊記憶體(真正的物件存放在堆上),其中儲存了name等資料 , 而student只是儲存了該物件的地址(存放在棧上)
當我們修改變數時
void test1(){
int cnt = 0;
cnt=1;
String str = new String("melo");
str="Melo";
}
對於基本型別 cnt,賦值運算子會直接改變變數的值,原來的值直接被覆蓋掉了。
ta無依無靠,不像下邊一樣有房子可以住。
對於引用型別 str,賦值運算子只會改變引用中所儲存的地址,雖然原來的地址被覆蓋掉了,str指向了一個新的物件,但是原來的那個老物件沒有發生變化,他還是老老實實待在原來的地方!!!
有學過c語言的同學應該很清楚,這裡藉助c語言中的“指標”打個比喻。
- 引用型別str就相當於一個指標(旗子),插在了一個房子門口。現在給這個旗子挪個位置,只是讓這個旗子放置在了另一個新的房子,原本的老房子還在那裡,不會說因為你改變了旗子的位置,房子就塌了。
當然,原來那個房子沒有旗子插著了,沒有人住了。也不能總是放任ta在那佔著空間,過段時間也許就會有人來把他給拆了回收了(JVM)。
這種沒有地方引用到的物件就稱為垃圾物件。
值傳遞
我們上次聊到lambda的時候,提及到了值傳遞,那裡的拷貝副本,就是我們這裡要說的值傳遞
- 如果我們這裡的方法塊訪問了外部的變數,而這個變數只是一個普通資料型別的話,相當於只是訪問到了一份副本。當外部對這個變數進行修改時,lambda內部(只有副本)是無法感知到這個變數的修改的。
我們只是將實參傳遞給了方法的形參,將cnt值複製一份,賦值給形參val所以,函式內對形參的操作完全不會影響到實參真正存活的區域!而伴隨著函式呼叫的結束,形參區域和其內的區域性變數也會被釋放。(方法棧的回收)
//基本型別的值傳遞
void unChange(int val) {
val = 100;
}
unChange(cnt); // cnt 並沒有被改變
引用傳遞
實參傳遞給形參時,形參其實用的就是實參本身(而不再單純只是拷貝一份副本出來了),當該形參變數被修改時,實參變數也會同步修改。
Java中到底是引用傳遞還是值傳遞呢
內卷例項
//內卷
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());
}
- 這裡看起來,好像符合我們引用傳遞的定義誒?
- 對形參temp的修改,會反饋到外部實參student那裡去?看起來操作的是同一個變數的樣子?
反內卷例項
看下邊這段"反內卷"的程式碼例項
//反內卷
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());
}
- 細心的同學可能發現了,我們這裡多了一步操作 --> temp = new Student();
先給出答案吧,Java裡邊其實只有值傳遞!!!
-
為什麼這麼說?
其實我們這裡的形參temp,只是拷貝了一份student的地址。可以理解為temp拷貝了這條指標,他也指向了student所指向的物件。 -
也就是說,temp只是跟student同樣指向了一個同一個物件而已,在第一個例子中,我們沒有去重新修改temp的指向,所以會造成一種假象:我們對temp的修改似乎等價於對student的修改? 其實只是剛好兩個指向了同一個物件而已!!
- 而如果我們對temp重新賦值了呢, temp = new Student();
- 對temp重新賦值後,此時temp就指向了另一個區域了,後續再對temp修改,根本不會影響原來的student指向的區域
所以才會"反內卷"失敗,跳出函式的時候,student所指向的物件成績根本沒有增長!!!
為什麼會有誤區呢?
- 其實還是因為Java中資料型別的問題,基本資料型別看起來就像是值傳遞,而引用傳遞因為存放了地址,讓我們能夠訪問到實參所指向的物件,容易讓我們誤以為我們的形參其實就等價於實參.
其他語言的引用
JS只有值傳遞,類似Java
指標傳遞(C語言)
注意指標傳遞跟引用傳遞是不一樣的
- 拿最老套的C語言手寫swap來講
#include <stdio.h>
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++)
#include <iostream>
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到底是引用傳遞還是值傳遞這個問題。我們只需要理解好本質就好了,通過上邊的那兩幅圖,理解好本質才是關鍵,萬變不離其宗。