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

Melo~發表於2021-11-05

寫在前邊

  • 上次聊到Java8新特性 lambda時,有小夥伴在評論區提及到了lambda對於區域性變數的引用,補充著部落格的時候,知識點一發散就有了這篇對於值傳遞還是引用傳遞的思考。關於這個問題為何會有如此多的誤區,這篇就來破解ta!

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

知識儲備--堆和棧

  • 堆是指動態分配記憶體的一塊區域,一般由程式設計師手動分配,比如 Java 中的 new、c裡邊的malloc。
  • 棧是編譯器幫我們分配好的區域,一般用於存放函式的引數值,區域性變數等

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

資料型別

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

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

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

void test1(){
      int cnt = 0;
      String str = new String("melo");
}

image.png

  1. cnt是基本型別,值就直接儲存在變數中(存放在棧上)
  2. 而str是引用型別,變數中儲存的只是實際物件的地址。一般稱這種變數為"引用",引用指向實際物件,實際物件中儲存著內容

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

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

當我們修改變數時

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

image.png
對於基本型別 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());
    }

image.png

  • 這裡看起來,好像符合我們引用傳遞的定義誒?
    • 對形參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());
    }

image.png

  • 細心的同學可能發現了,我們這裡多了一步操作 --> temp = new Student();

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

  • 為什麼這麼說?
    其實我們這裡的形參temp,只是拷貝了一份student的地址。可以理解為temp拷貝了這條指標,他也指向了student所指向的物件。

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

image.png

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

image.png

  • 對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到底是引用傳遞還是值傳遞這個問題。我們只需要理解好本質就好了,通過上邊的那兩幅圖,理解好本質才是關鍵,萬變不離其宗。

相關文章