難住了同事:Java 方法呼叫到底是傳值還是傳引用
阿新 • • 發佈:2020-03-05
> **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 的世界中沒有所謂的指標傳遞。
> 人在江湖,各位捧個贊場,輕輕點個推薦吧