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

數據類型和內存分配

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

基本類型

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

<code>int i = 89;/<code>

會在虛擬機棧上分配 4 個字節的空間出來存放。

引用類型

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

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

<code>User user = new User();/<code>

會經歷如下三個步驟:

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

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

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

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

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

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

沒有爭議的基本類型

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

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

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

改變參數值並不會改變原變量的值,沒錯吧,Java 是按值傳遞。

數組和類

數組

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

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

這段代碼的輸出是

<code>1
100/<code>

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

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

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

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

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

<code>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 +
'}';
}
}/<code>

比方說我們有一個原始的實體 User 類對象,將這個實體對象傳給一個方法,這個方法可能會有一些邏輯處理,比如我們拿到這個用戶的 name 屬性,發現 name 為空,我們就給 name 屬性賦予一個隨機名稱,例如 “用戶398988”。這應該是很常見的一類場景了。

我們通常這樣使用,將 user 實例當做參數傳過來,處理完成後,再將它返回。

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

但有的同學說,我發現修改完成後就算不返回,原變量 oldUser 的屬性也改變了,比如下面這樣:

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

返回的結果都是下面這樣

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

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

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

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

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

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

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

假設就是引用傳遞,那麼 newUser 和 main 方法中的 oldUser 就是同一個引用對象,那我在 wantChangeUser 方法中重新 new 了一個 User 實體,並賦值給了 newUser,按照引用傳遞這個說法,我賦值給了參數也就是賦值給了原始變量,那麼當完成賦值操作後,原變量 oldUser 就應該是 name = "新名字"、age=18 才對。

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

<code>User{name='原始姓名', age=8}
User{name='原始姓名', age=8}/<code>

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

結論

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

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


分享到:


相關文章: