面試官:兄弟,說說Java到底是值傳遞還是引用傳遞

沉默王二發表於2020-04-23

二哥,好久沒更新面試官系列的文章了啊,真的是把我等著急了,所以特意過來催催。我最近一段時間在找工作,能從二哥的文章中學到一點就多一點信心啊!

說句實在話,離讀者 trust you 發給我這段資訊已經過去 1 周時間了。不是我怠慢,確實是可更新的內容實在是太多了。這不,又有兩個讀者不約而同地要求我更新一下 Java 到底是值傳遞還是引用傳遞方面的文章——其實這個問題我之前是寫過的,但現在看起來答案似乎不夠盡善盡美,所以打算以面試的角度重寫一篇。

七年前,我從溫和溼潤的蘇州回到古色古香的洛陽,抱著一幅“天下我有”的心態“約談”了幾位面試官。其中有一位叫老馬,讓我印象深刻。因為他當時扔了一個面試題把我砸懵了:說說 Java 到底是值傳遞還是引用傳遞吧。

我當時年輕氣盛,自認為所有的面試題都能對答如流,沒想到被老馬“刁難”了——原來洛陽這塊網際網路的荒漠也有技術專家啊。現在回想起來,臉上不自覺地就泛起了羞愧的紅暈:當時真菜!不管怎麼說,是時候寫篇文章剖析一下值傳遞還是引用傳遞的區別了。

將引數傳遞給方法有兩種常見的方式,一種是“值傳遞”,一種是“引用傳遞”。C 語言本身只支援值傳遞,它的衍生品 C++ 既支援值傳遞,也支援引用傳遞,而 Java 只支援值傳遞。

01、值傳遞 VS 引用傳遞

首先,我們必須要搞清楚,到底什麼是值傳遞,什麼是引用傳遞,否則,討論 Java 到底是值傳遞還是引用傳遞就顯得毫無意義。

當一個引數按照值的方式在兩個方法之間傳遞時,呼叫者和被呼叫者其實是用的兩個不同的變數——被呼叫者中的變數(原始值)是呼叫者中變數的一份拷貝,對它們當中的任何一個變數修改都不會影響到另外一個變數。

而當一個引數按照引用傳遞的方式在兩個方法之間傳遞時,呼叫者和被呼叫者其實用的是同一個變數,當該變數被修改時,雙方都是可見的。

Java 程式設計師之所以容易搞混值傳遞和引用傳遞,主要是因為 Java 有兩種資料型別,一種是基本型別,比如說 int,另外一種是引用型別,比如說 String。

基本型別的變數儲存的都是實際的值,而引用型別的變數儲存的是物件的引用——指向了物件在記憶體中的地址。值和引用儲存在 stack(棧)中,而物件儲存在 heap(堆)中。

之所以有這個區別,是因為:

  • 棧的優勢是,存取速度比堆要快,僅次於直接位於 CPU 中的暫存器。但缺點是,棧中的資料大小與生存週期必須是確定的。
  • 堆的優勢是可以動態地分配記憶體大小,生存週期也不必事先告訴編譯器,Java 的垃圾回收器會自動收走那些不再使用的資料。但由於要在執行時動態分配記憶體,存取速度較慢。

02、基本型別的引數傳遞

眾所周知,Java 有 8 種基本資料型別,分別是 int、long、byte、short、float、double 、char 和 boolean。它們的值直接儲存在棧中,每當作為引數傳遞時,都會將原始值(實參)複製一份新的出來,給形參用。形參將會在被呼叫方法結束時從棧中清除。

來看下面這段程式碼:

public class PrimitiveTypeDemo {
    public static void main(String[] args) {
        int age = 18;
        modify(age);
        System.out.println(age);
    }

    private static void modify(int age1) {
        age1 = 30;
    }
}

1)main 方法中的 age 是基本型別,所以它的值 18 直接儲存在棧中。

2)呼叫 modify() 方法的時候,將為實參 age 建立一個副本(形參 age1),它的值也為 18,不過是在棧中的其他位置。

3)對形參 age 的任何修改都只會影響它自身而不會影響實參。

03、引用型別的引數傳遞

來看一段建立引用型別變數的程式碼:

Writer writer = new Writer(18"沉默王二");

writer 是物件嗎?還是物件的引用?為了搞清楚這個問題,我們可以把上面的程式碼拆分為兩行程式碼:

Writer writer;
writer = new Writer(18"沉默王二");

假如 writer 是物件的話,就不需要通過 new 關鍵字建立物件了,對吧?那也就是說,writer 並不是物件,在“=”操作符執行之前,它僅僅是一個變數。那誰是物件呢?new Writer(18, "沉默王二"),它是物件,儲存於堆中;然後,“=”操作符將物件的引用賦值給了 writer 變數,於是 writer 此時應該叫物件引用,它儲存在棧中,儲存了物件在堆中的地址。

每當引用型別作為引數傳遞時,都會建立一個物件引用(實參)的副本(形參),該形參儲存的地址和實參一樣。

來看下面這段程式碼:

public class ReferenceTypeDemo {
    public static void main(String[] args) {
        Writer a = new Writer(18);
        Writer b = new Writer(18);
        modify(a, b);

        System.out.println(a.getAge());
        System.out.println(b.getAge());
    }

    private static void modify(Writer a1, Writer b1) {
        a1.setAge(30);

        b1 = new Writer(18);
        b1.setAge(30);
    }
}

1)在呼叫 modify() 方法之前,實參 a 和 b 指向的物件是不一樣的,儘管 age 都為 18。

2)在呼叫 modify() 方法時,實參 a 和 b 都在棧中建立了一個新的副本,分別是 a1 和 b1,但指向的物件是一致的(a 和 a1 指向物件 a,b 和 b1 指向物件 b)。

3)在 modify() 方法中,修改了形參 a1 的 age 為 30,意味著物件 a 的 age 從 18 變成了 30,而實參 a 指向的也是物件 a,所以 a 的 age 也變成了 30;形參 b1 指向了一個新的物件,隨後 b1 的 age 被修改為 30。

修改 a1 的 age,意味著同時修改了 a 的 age,因為它們指向的物件是一個;修改 b1 的 age,對 b 卻沒有影響,因為它們指向的物件是兩個。

程式輸出的結果如下所示:

30
18

果然和我們的分析是吻合的。

04、最後

好了,我親愛的讀者朋友,以上就是本文的全部內容了。看完之後,再遇到面試官問 Java 到底是值傳遞還是引用傳遞時,就不用擔心被刁難了。我是沉默王二,一枚有趣的程式設計師。原創不易,莫要白票,請你為本文點贊個吧,這將是我寫作更多優質文章的最強動力。

如果覺得文章對你有點幫助,請微信搜尋「 沉默王二 」第一時間閱讀,回覆【666】更有我為你精心準備的 500G 高清教學視訊(已分門別類)。本文 GitHub 已經收錄,有大廠面試完整考點,歡迎 Star。

相關文章