為什麼大家都說Java中只有值傳遞?
最近跟Java中的值傳遞和引用傳遞槓上了,一度懷疑人生。查了很多資料,加上自己的理解,終於搞清楚了,什麼是值傳遞和引用傳遞。也搞明白了,為什麼大家都說Java只有值傳遞,沒有引用傳遞。原來,我一直以來的認知都是錯誤的。。。
首先,需要了解一些概念性的東西。
形參與實參:
形參,是指在定義函式時使用的引數,目的是用於接收呼叫該函式時傳入的引數。簡單理解,就是所有函式(即方法)的引數都是形參。
實參,是指呼叫函式時,傳遞給函式的引數。
public static void main(String[] args) { int num = 3; printVal(num); //這裡num是實參 } private static void printVal(int num) { num = 5; //這裡num就是形參 }
值傳遞和引用傳遞
值傳遞:是指在呼叫函式時,將實際引數複製一份傳遞給函式,這樣在函式中修改引數時,不會影響到實際引數。其實,就是在說值傳遞時,只會改變形參,不會改變實參。
引用傳遞:是指在呼叫函式時,將實際引數的地址傳遞給函式,這樣在函式中對引數的修改,將影響到實際引數。
這裡,需要特別強調的是,千萬不要以為傳遞的引數是值就是值傳遞,傳遞的是引用就是引用傳遞。也不要以為傳遞的引數是基本資料型別就是值傳遞,傳遞的是物件就是引用傳遞。 這是大錯特錯的。以前的我,一直都是這樣認為的,現在想來真是太天真了。判斷是值傳遞還是引用傳遞的標準,和傳遞引數的型別是沒有一毛錢關係的。
下面三種情況,基本上可以涵蓋所有情況的引數型別。
當傳遞的引數是基本資料型別時:
public class TestNum { public static void main(String[] args) { int num = 3; System.out.println("修改前的num值:"+num); changeValue(num); System.out.println("修改後的num值:"+num); } private static void changeValue(int num) { num = 5; System.out.println("形參num值:"+num); } }
列印結果:
修改前的num值:3
形參num值:5
修改後的num值:3
可以發現,傳遞基本資料型別時,在函式中修改的僅僅是形參,對實參的值的沒有影響。
需要明白一點,值傳遞不是簡單的把實參傳遞給形參,而是,實參建立了一個副本,然後把副本傳遞給了形參。下面用圖來說明一下引數傳遞的過程:
圖中num是實參,然後建立了一個副本temp,把它傳遞個形參value,修改value值對實參num沒有任何影響。
傳遞型別是引用型別時:
public class User {
private int age;
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public User(int age, String name) {
this.age = age;
this.name = name;
}
public User() {
}
@Override
public String toString() {
return "User{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
public class TestUser {
public static void main(String[] args) {
User user = new User(18, "zhangsan");
System.out.println("修改物件前:"+user);
changeUser(user);
System.out.println("修改物件後:"+user);
}
private static void changeUser(User user) {
user.setAge(20);
user.setName("lisi");
}
}
列印結果:
修改物件前:User{age=18, name='zhangsan'}
修改物件後:User{age=20, name='lisi'}
可以發現,傳過去的user物件,屬性值被改變了。由於,user物件存放在堆裡邊,其引用存放在棧裡邊,其引數傳遞圖如下:
user是物件的引用,為實參,然後建立一個副本temp,把它傳遞給形參user1。但是,他們實際操作的都是堆記憶體中的同一個User物件。因此,物件內容的修改也會體現到實參user上。
傳遞型別是String型別(Integer等基本型別的包裝類等同)
public class TestStr {
public static void main(String[] args) {
String str = new String("zhangsan");
System.out.println("字串修改前:"+str);
changeStr(str);
System.out.println("字串修改後:"+str);
}
private static void changeStr(String str) {
str = "lisi";
}
}
列印結果:
字串修改前:zhangsan
字串修改後:zhangsan
咦,看到這是不是感覺有點困惑。按照第二種情況,傳遞引數是引用型別時,不是可以修改物件內容嗎,String也是引用型別,為什麼在這又不變了呢?
再次強調一下,傳遞引數是引用型別,並不代表就是引用傳遞,其實它還是值傳遞。此時的 lisi 和上邊的 zhangsan 根本不是同一個物件。畫圖理解下:
圖中,str是物件 zhangsan 的引用,為實參,然後建立了一個副本temp,把它傳遞給了形參str1。此時,建立了一個新的物件 lisi ,形參str1指向這個物件,但是原來的實參str還是指向zhangsan。因此,形參內容的修改並不會影響到實參內容。所以,兩次列印結果都是zhangsan。
第三種情況和第二種情況雖然傳遞的都是引用型別變數,但是處理方式卻不一樣。第三種情況是建立了一個新的物件,然後把形參指向新物件,而第二種情況並沒有建立新物件,操作的還是同一個物件。如果把上邊changeUser方法稍作改變,你就會理解:
private static void changeUser(User user) {
//新增一行程式碼,建立新的User物件
user = new User();
user.setAge(20);
user.setName("lisi");
}
執行以上程式碼,你就會驚奇的發現,最終列印修改前和修改後的內容是一模一樣的。
這種情況,就等同於第三種情況。因為,這裡的形參和實參引用所指向的物件是不同的物件。因此,修改形參物件內容並不會影響實參內容。
修改物件前:User{age=18, name='zhangsan'}
修改物件後:User{age=18, name='zhangsan'}
總結:
從以上三個例子中,我們就能理解了,為什麼Java中只有值傳遞,並沒有引用傳遞。值傳遞,不論傳遞的引數型別是值型別還是引用型別,都會在呼叫棧上建立一個形參的副本。不同的是,對於值型別來說,複製的就是整個原始值的複製。而對於引用型別來說,由於在呼叫棧中只儲存物件的引用,因此複製的只是這個引用,而不是原始物件。
最後,再次強調一下,傳遞引數是引用型別,或者說是物件時,並不代表它就是引用傳遞。引用傳遞不是用來形容引數的型別的,不要被“引用”這個詞本身迷惑了。這就如同我們生活中說的地瓜不是瓜,而是紅薯一樣。
- 引數傳遞時,是拷貝實參的副本,然後傳遞給形參。(值傳遞)
- 在函式中,只有修改了實參所指向的物件內容,才會影響到實參。以上第三種情況修改的實際上只是形參所指向的物件,因此不會影響實參。