­

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

Java 方法調用中的參數是值傳遞還是引用傳遞呢?相信每個做開發的同學都碰到過傳這個問題,不光是做 Java 的同學,用 C#、Python 開發的同學同樣肯定遇到過這個問題,而且很有可能不止一次。

那麼,Java 中到底是值傳遞還是引用傳遞呢,答案是值傳遞,Java 中沒有引用傳遞這個概念。

數據類型和記憶體分配

Java 中有可以概括為兩大類數據類型,一類是基本類型,另一類是引用類型。

基本類型

byte、short、int、long、float、double、char、boolean 是 Java 中的八種基本類型。基本類型的記憶體分配在棧上完成,也就是 JVM 的虛擬機棧。也就是說,當你使用如下語句時:

int i = 89;

會在虛擬機棧上分配 4 個位元組的空間出來存放。

引用類型

引用類型有類、介面、數組以及 null 。我們平時熟悉的各種自定義的實體類啊就在這個範疇里。

當我們定義一個對象並且使用 new 關鍵字來實例化對象時。

User user = new User();

會經歷如下三個步驟:

1、聲明一個引用變數 user,在虛擬機棧上分配空間;

2、使用 new 關鍵字創建對象實例,在堆上分配空間存放對象內的屬性資訊;

3、將堆上的對象鏈接到 user 變數上,所以棧上存儲的實際上就是存的對象在堆上的地址資訊;

數組對象也是一樣的,棧上只是存了一個地址,指向堆上實際分配的數組空間,實際的值是存在堆上的。

為了清楚的展示空間分配,我畫了一張類型空間分配的示例圖。

沒有爭議的基本類型

當我們將 8 種基本類型作為方法參數傳遞時,沒有爭議,傳的是什麼(也就是實參),方法中接收的就是什麼(也就是形參)。傳遞過去的是 1 ,那接到的就是1,傳過去的是 true,接收到的也就是 true。

看下面這個例子,將變數 oldIntValue 傳給 changeIntValue 方法,在方法內對參數值進行修改,最後輸出的結果還是 1。

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 是按值傳遞。

數組和類

數組

有的同學說那不對呀,你看我下面這段程式碼,就不是這樣。

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;  }

這段程式碼的輸出是

1  100

說明調用 changeArrayValue 方法時,修改傳過來的數組參數中的第一項後,原變數的內容改變了,那這怎麼是值傳遞呢。

別急,看看下面這張圖,展示了數組在 JVM 中的記憶體分配示例圖。

實際上可以理解為 changeArrayValue 方法接收的參數是原變數 oldArray 的副本拷貝,只不過數組引用中存的只是指向堆中數組空間的首地址而已,所以,當調用 changeArrayValue 方法後,就形成了 oldArray 和 newArray 兩個變數在棧中的引用地址都指向了同一個數組地址。所以修改參數的每個元素就相當於修改了原變數的元素。

一般我們在開發過程中有很多將類實例作為參數的情況,我們抽象出來的各種對象經常在方法間傳遞。比如我們定義了一個用戶實體類。

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 實例當做參數傳過來,處理完成後,再將它返回。

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 的屬性也改變了,比如下面這樣:

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 );  }

返回的結果都是下面這樣

User{name='原始姓名', age=8}  User{name='新名字', age=18}

那這不就是引用傳遞嗎,改了參數的屬性,就改了原變數的屬性。仍然來看一張圖

實際上仍然不是引用傳遞,引用傳遞我們學習 C++ 的時候經常會用到,就是指針。而這裡傳遞的其實是一個副本,副本中只存了指向堆空間對象實體的地址而已。我們我們修改參數 newUser 的屬性間接的就是修改了原變數的屬性。

有同學說,那畫一張圖說這樣就是這樣嗎,你說是副本就是副本嗎,我偏說就是傳的引用,就是原變數,也說得通啊。

確實是說的通,如果真是引用傳遞,也確實是這樣的效果沒錯。那我們就來個反例。

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 才對。

然後,我們運行看看輸出結果:

User{name='原始姓名', age=8}  User{name='原始姓名', age=8}

結果依然是修改前的值,我們修改了 newUser ,並沒有影響到原變數,顯然不是引用傳遞。

結論

Java 中的參數傳遞是值傳遞,並且 Java 中沒有引用傳遞這個概念。我們通常說的引用傳遞,一般都是從 C 語言和 C like 而來,因為它們有指針的概念。

而我們也知道,C、C++ 中需要程式設計師自己管理記憶體,而指針的使用經常會導致記憶體泄漏一類的問題,Java 千辛萬苦的就是為了讓程式設計師解放出來,而使用垃圾收集策略管理記憶體,這其中很重要的一點就是規避了指針的使用,所以在 Java 的世界中沒有所謂的指針傳遞。

人在江湖,各位捧個贊場,輕輕點個推薦吧 ?


公眾號:古時的風箏

一個斜杠程式設計師,一個純粹的技術公眾號,多寫 Java 相關技術文章,不排除會寫其他內容。