1. 程式人生 > >為什麼大家都說Java中只有值傳遞?

為什麼大家都說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中只有值傳遞,並沒有引用傳遞。值傳遞,不論傳遞的引數型別是值型別還是引用型別,都會在呼叫棧上建立一個形參的副本。不同的是,對於值型別來說,複製的就是整個原始值的複製。而對於引用型別來說,由於在呼叫棧中只儲存物件的引用,因此複製的只是這個引用,而不是原始物件。

最後,再次強調一下,傳遞引數是引用型別,或者說是物件時,並不代表它就是引用傳遞。引用傳遞不是用來形容引數的型別的,不要被“引用”這個詞本身迷惑了。這就如同我們生活中說的地瓜不是瓜,而是紅薯一樣。

  1. 引數傳遞時,是拷貝實參的副本,然後傳遞給形參。(值傳遞)
  2. 在函式中,只有修改了實參所指向的物件內容,才會影響到實參。以上第三種情況修改的實際上只是形參所指向的物件,因此不會影響實參。