聊聊Java裏面的引用傳遞

長久以來,在Java語言裏面一直有一個爭論,就是Java語言到底是值傳遞(pass-by-value)還是引用傳遞(pass-by-reference),有的人說是值傳遞,有的人說是引用傳遞,兩邊各執一詞,從而誤導了很多開發者,更有甚者告訴開發者說不必糾結Java到底是值傳遞還是引用傳遞,只要能用就行了,但事實真的是這樣嗎? 答案是否定的。

爲了弄清這兩個概念,我們先要理解這兩個概念本身的意思:

首先值傳遞本身這個名字,我感覺就誤導了不少人,對於值傳遞你可以理解成是數據的內存地址傳遞,並非是數據本身,或者叫它是指針。在維基百科裏面,關於指針有清晰的描述:

In computer science, a pointer is a programming language object that stores the memory address of another value located in computer memory. A pointer references a location in memory。

然後,我們看引用傳遞中的引用的在維基百科上的描述:

In computer science, a reference is a value that enables a program to indirectly access a particular datum, such as a variable's value or a record, in the computer's memory or in some other storage device.

簡單的說,引用本身就代表了數據,改變引用相當於改變了數據本身。

根據概念的定義再回到Java語言裏面,就會發現對Java本身來說,它只有指針傳遞也就是值傳遞,並非是引用傳遞。到這裏,我相信有一部分讀者可能已經接受不了,因爲在Java裏面大多數時候,我們都是講基本類型,引用類型,從沒聽過什麼指針的概念。導致這樣的原因其實跟早期的Sun公司宣傳策略有關係,我們知道Java語言,其實是從C語言借鑑改進過來的,其中一個主要的優點就是不允許我們像C那樣可以通過指針直接操作內存區域,爲了這個事情,Sun公司替換了概念稱Java的指針爲引用,從而在Java語言的發展歷程中導致了更大的困惑,持續演變至今。事實就是Sun公司命名錯誤,當然Java已經發展了很多年,Oracle接盤後也犯不上糾正命名的問題。結果就是懂的人永遠都知道是怎麼回事,不懂的人一直分不清這兩個概念。

只有認清了Java裏面存在指針,承認指針,我們才能更加自信的理解Java語言。

我們來看一個簡單的例子:

class Dog{
       public String name;

       public Dog() {
       }

       public Dog(String name) {
            this.name = name;
        }
    }

然後,我們試着創建一個Dog對象:

Dog dog=null; //1
System.out.println(dog.name);
dog=new Dog();//2

然後運行一下,我們會看到一個異常:

Exception in thread "main" java.lang.NullPointerException

注意這個異常,叫空指針異常,在Java裏面任何對象沒有初始化的時候,如果我們使用其內部屬性,就會拋出上面的信息,這也從側面反映了dog這個變量的作用,其實就是指針,而並非引用。對於已經實例化的對象,如果我們沒有重寫toString方法,打印指針會得到一串類名組合+內存地址轉換的十六進制字符串。

我們接着來看一個例子:

public static void change(String point){
       point="new value";
    }

    public static void swapString(){
        String orgin="orgin";
        System.out.println(orgin);
        change(orgin);
        System.out.println("==================after==================");
        System.out.println(orgin);


    }

如果在main方法裏面,執行swapString()方法,輸出的結果都是orgin。

對於Dog對象也一樣,如下:

public static void change(Dog dog){
       dog=new Dog("CAT");
       dog.name="cat";
    }


    public static void swapDog(){
        Dog dog=new Dog("tom");
        System.out.println(dog.name);
        change(dog);
        System.out.println("==================after==================");
        System.out.println(dog.name);
    }

最終輸出的結果都是tom,好像從未執行過change方法一樣。

tom
==================after==================
tom

這是爲什麼? 你可能要說很簡單啊,方法裏面的作用域,只在方法裏生效,出了方法就無效了。真的是這樣嗎? 接着看下面,我們稍微改動一下change方法:

public static void change(Dog dog){
       dog.name="new_tom";
       dog=new Dog("CAT");
       dog.name="cat";
    }

同樣,在main方法執行,輸出的結果是:

tom
==================after==================
new_tom

這裏面,如果不理解值傳遞(指針傳遞)和引用傳遞的區別,其實是很難明白原因的:

public static void change(Dog dog){
       dog=new Dog("CAT");//3 memory location:8888
       dog.name="cat";//4
    }


    public static void swapDog(){
        Dog dog=new Dog("tom");//1  memory location:7777
        System.out.println(dog.name);
        change(dog);//2
        System.out.println("==================after==================");
        System.out.println(dog.name);
    }

我們加上序號之後,來分析一下,首先在第一步,我們創建了一個Dog對象,其中dog變量是指針(或者按Java的習慣叫引用,但其實是不正確的),指針存的是內存地址,而並非是內存中的數據本身,我們假設內存地址是7777,然後在第二步,將dog(指針)引用,傳給了change方法,在第三步,因爲我們新創建了一個Dog,並把dog指針指向了新的對象,這裏假設其在堆上的內存地址是8888,然後並給內存地址=8888的Dog對象的name賦值了cat,然後方法執行結束,最後回到swapDog方法,繼續執行,此時swapDog方法dog對象的指針仍然是7777,所以並沒有任何變化。

圖示如下:

注意change方法執行後,沒有指針指向內存地址8888的對象,故會在下一次gc時回收。

接着我們分析下,微調後的change方法:

public static void change(Dog dog){
       dog.name="new_tom";// 2.1
       dog=new Dog("CAT");
       dog.name="cat";
    }

在第2.1步,我們通過dog指針=7777的數據,重新改變了其名稱,這意味着內存地址7777的數據,被修改了,後面的兩行改的是內存地址=8888的數據,所以最終結果是改變後的。

注意,如果Java語言是引用傳遞的話,那麼最終的結果name肯定是cat,因爲引用傳遞的修改,指的就是數據本身,而並非地址。

上面關於值傳遞(指針傳遞)和引用傳遞,說的有點抽象,我打個比方:

指針指的是你的名字,通過指針可以找到數據本身,然後操作數據,但如果指針本身(非數據本身)變了,也就是你名字變了,但其實跟你沒有關係,你自己還是你自己,你可以重新在換個名字代表你。

引用指的是你本身,如果本身變了,比如你換了個髮型,這個時候,無論你換多個名字(指針),你都一樣是你,改變不了你髮型換了的事實。

所以,這個時候如果按照值傳遞(指針傳遞)的理解,來看上面的例子,你就會恍然大悟。在change方法裏面dog的指針已經被替換成了8888,而8888地址代表的是新的對象 所以不會改變7777的對象的內容,在微調後的版本中,我們直接改變了7777地址數據的name,所以最終的結果也是改變後的。

總結:

Java語言本身是值傳遞,也叫做指針傳遞,雖然我們一直叫引用類型,但其實它實際上是一個指針,而真正的引用傳遞改變的是數據本身的內容,如Lisp和Fortran語言,無論哪種方式,我們只要理解了其本質,就可以掌握的更好。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章