1. 程式人生 > >難住了同事:Java 方法呼叫到底是傳值還是傳引用

難住了同事:Java 方法呼叫到底是傳值還是傳引用

> **Java 方法呼叫中的引數是值傳遞還是引用傳遞呢?**相信每個做開發的同學都碰到過傳這個問題,不光是做 Java 的同學,用 C#、Python 開發的同學同樣肯定遇到過這個問題,而且很有可能不止一次。 > > 那麼,Java 中到底是值傳遞還是引用傳遞呢,**答案是值傳遞,Java 中沒有引用傳遞這個概念。** ### 資料型別和記憶體分配 Java 中有可以概括為兩大類資料型別,一類是基本型別,另一類是引用型別。 **基本型別** byte、short、int、long、float、double、char、boolean 是 Java 中的八種基本型別。基本型別的記憶體分配在棧上完成,也就是 JVM 的虛擬機器棧。也就是說,當你使用如下語句時: ```java int i = 89; ``` 會在虛擬機器棧上分配 4 個位元組的空間出來存放。 **引用型別** 引用型別有類、介面、陣列以及 null 。我們平時熟悉的各種自定義的實體類啊就在這個範疇裡。 當我們定義一個物件並且使用 new 關鍵字來例項化物件時。 ```java User user = new User(); ``` 會經歷如下三個步驟: 1、宣告一個引用變數 user,在虛擬機器棧上分配空間; 2、使用 new 關鍵字建立物件例項,在堆上分配空間存放物件內的屬性資訊; 3、將堆上的物件連結到 user 變數上,所以棧上儲存的實際上就是存的物件在堆上的地址資訊; 陣列物件也是一樣的,棧上只是存了一個地址,指向堆上實際分配的陣列空間,實際的值是存在堆上的。 為了清楚的展示空間分配,我畫了一張型別空間分配的示例圖。 ![](https://img2020.cnblogs.com/blog/273364/202003/273364-20200305110626395-2019826808.png) ### 沒有爭議的基本型別 當我們將 8 種基本型別作為方法引數傳遞時,沒有爭議,傳的是什麼(也就是實參),方法中接收的就是什麼(也就是形參)。傳遞過去的是 1 ,那接到的就是1,傳過去的是 true,接收到的也就是 true。 看下面這個例子,將變數 oldIntValue 傳給 changeIntValue 方法,在方法內對引數值進行修改,最後輸出的結果還是 1。 ```java public static void main( String[] args ) throws Exception{ int oldIntValue = 1; System.out.println( oldIntValue ); passByValueOrRef.changeIntValue( oldIntValue ); System.out.println( oldIntValue ); } public static void changeIntValue( int oldValue ){ int newValue = 100; oldValue = newValue; } ``` 改變引數值並不會改變原變數的值,沒錯吧,Java 是按值傳遞。 ### 陣列和類 **陣列** 有的同學說那不對呀,你看我下面這段程式碼,就不是這樣。 ```java public static void main( String[] args ) throws Exception{ int[] oldArray = new int[] { 1, 2 }; System.out.println( oldArray[0] ); changeArrayValue( oldArray ); System.out.println( oldArray[0] ); } public static void changeArrayValue( int[] newArray ){ newArray[0] = 100; } ``` 這段程式碼的輸出是 ```shell 1 100 ``` 說明呼叫 changeArrayValue 方法時,修改傳過來的陣列引數中的第一項後,原變數的內容改變了,那這怎麼是值傳遞呢。 別急,看看下面這張圖,展示了陣列在 JVM 中的記憶體分配示例圖。 ![](https://img2020.cnblogs.com/blog/273364/202003/273364-20200305110643673-430684053.png) 實際上可以理解為 changeArrayValue 方法接收的引數是原變數 oldArray 的副本拷貝,只不過陣列引用中存的只是指向堆中陣列空間的首地址而已,所以,當呼叫 changeArrayValue 方法後,就形成了 oldArray 和 newArray 兩個變數在棧中的引用地址都指向了同一個陣列地址。所以修改引數的每個元素就相當於修改了原變數的元素。 **類** 一般我們在開發過程中有很多將類例項作為引數的情況,我們抽象出來的各種物件經常在方法間傳遞。比如我們定義了一個使用者實體類。 ```java public class User { private String name; private int age; public User(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "User{" + "name='" + name + '\'' + ", age=" + age + '}'; } } ``` 比方說我們有一個原始的實體 User 類物件,將這個實體物件傳給一個方法,這個方法可能會有一些邏輯處理,比如我們拿到這個使用者的 name 屬性,發現 name 為空,我們就給 name 屬性賦予一個隨機名稱,例如 “使用者398988”。這應該是很常見的一類場景了。 我們通常這樣使用,將 user 例項當做引數傳過來,處理完成後,再將它返回。 ```java public static void main( String[] args ) throws Exception{ User oldUser = new User( "原始姓名", 8 ); System.out.println( oldUser.toString() ); oldUser = changeUserValue( oldUser ); System.out.println( oldUser.toString() ); } public static User changeUserValue( User newUser ){ newUser.setName( "新名字" ); newUser.setAge( 18 ); return newUser; } ``` 但有的同學說,我發現修改完成後就算不返回,原變數 oldUser 的屬性也改變了,比如下面這樣: ```java public static void main( String[] args ) throws Exception{ User oldUser = new User( "原始姓名", 8 ); System.out.println( oldUser.toString() ); changeUserValue( oldUser ); System.out.println( oldUser.toString() ); } public static void changeUserValue( User newUser ){ newUser.setName( "新名字" ); newUser.setAge( 18 ); } ``` 返回的結果都是下面這樣 ```json User{name='原始姓名', age=8} User{name='新名字', age=18} ``` 那這不就是引用傳遞嗎,改了引數的屬性,就改了原變數的屬性。仍然來看一張圖 ![](https://img2020.cnblogs.com/blog/273364/202003/273364-20200305110710417-1437029179.png) 實際上仍然不是引用傳遞,引用傳遞我們學習 C++ 的時候經常會用到,就是指標。而這裡傳遞的其實是一個副本,副本中只存了指向堆空間物件實體的地址而已。我們我們修改引數 newUser 的屬性間接的就是修改了原變數的屬性。 有同學說,那畫一張圖說這樣就是這樣嗎,你說是副本就是副本嗎,我偏說就是傳的引用,就是原變數,也說得通啊。 確實是說的通,如果真是引用傳遞,也確實是這樣的效果沒錯。那我們就來個反例。 ```java public static void main( String[] args ) throws Exception{ User oldUser = new User( "原始姓名", 8 ); System.out.println( oldUser.toString() ); wantChangeUser( oldUser ); System.out.println( oldUser.toString() ); } public static void wantChangeUser( User newUser ){ newUser = new User( "新姓名", 18 ); } ``` 假設就是引用傳遞,那麼 newUser 和 main 方法中的 oldUser 就是同一個引用物件,那我在 wantChangeUser 方法中重新 new 了一個 User 實體,並賦值給了 newUser,按照引用傳遞這個說法,我賦值給了引數也就是賦值給了原始變數,那麼當完成賦值操作後,原變數 oldUser 就應該是 name = "新名字"、age=18 才對。 然後,我們執行看看輸出結果: ```json User{name='原始姓名', age=8} User{name='原始姓名', age=8} ``` 結果依然是修改前的值,我們修改了 newUser ,並沒有影響到原變數,顯然不是引用傳遞。 ### 結論 Java 中的引數傳遞是值傳遞,並且 Java 中沒有引用傳遞這個概念。我們通常說的引用傳遞,一般都是從 C 語言和 C like 而來,因為它們有指標的概念。 而我們也知道,C、C++ 中需要程式設計師自己管理記憶體,而指標的使用經常會導致記憶體洩漏一類的問題,Java 千辛萬苦的就是為了讓程式設計師解放出來,而使用垃圾收集策略管理記憶體,這其中很重要的一點就是規避了指標的使用,所以在 Java 的世界中沒有所謂的指標傳遞。 > 人在江湖,各位捧個贊場,輕輕點個推薦吧