reference教學 by call Java是“傳遞引用”還是“按值傳遞”?




15 Answers

我剛剛注意到你引用了我的文章

Java規範說Java中的所有內容都是按值傳遞的。 在Java中沒有“pass-by-reference”這樣的東西。

理解這一點的關鍵是類似的東西

Dog myDog;

不是狗; 它實際上是指向狗的指針

這意味著,就在你擁有的時候

Dog myDog = new Dog("Rover");
foo(myDog);

你實際上是將創建的Dog對象的地址傳遞給foo方法。

(我說的主要是因為Java指針不是直接地址,但最容易想到它們)

假設Dog對象駐留在內存地址42處。這意味著我們將42傳遞給該方法。

如果方法被定義為

public void foo(Dog someDog) {
    someDog.setName("Max");     // AAA
    someDog = new Dog("Fifi");  // BBB
    someDog.setName("Rowlf");   // CCC
}

讓我們來看看發生了什麼。

  • 參數someDog設置為值42
  • 在線“AAA”
    • someDog跟隨它指向的Dog (地址42處的Dog對象)
    • 要求Dog (地址為42的那個)將他的名字改為Max
  • 在線“BBB”
    • 創建了一隻新Dog 。 讓我們說他在地址74
    • 我們將參數someDog分配給74
  • 在線“CCC”
    • someDog跟隨它所指向的Dog (地址為74的Dog對象)
    • 要求Dog (地址為74的那個)將他的名字改為Rowlf
  • 然後,我們回來了

現在讓我們考慮一下方法之外會發生什麼:

myDog改變了嗎?

關鍵是。

記住myDog是一個指針 ,而不是一個真正的Dog ,答案是否定的。 myDog的值仍為42; 它仍然指向原始的Dog (但請注意,由於行“AAA”,它的名稱現在是“Max” - 仍然是相同的狗; myDog的值沒有改變。)

按照地址並改變最後的內容是完全有效的; 但是,這不會改變變量。

Java的工作方式與C完全相同。您可以分配指針,將指針傳遞給方法,按照方法中的指針操作並更改指向的數據。 但是,您無法更改指針指向的位置。

在C ++,Ada,Pascal和其他支持pass-by-reference的語言中,您實際上可以更改傳遞的變量。

如果Java具有pass-by-reference語義,那麼我們在上面定義的foo方法會更改someDog在BBB上分配someDog時指向的位置。

將引用參數視為傳入的變量的別名。分配該別名時,傳入的變量也是如此。

java reference用法

我一直認為Java是傳遞引用的

但是,我看過一些博客文章(例如, 這個博客 )聲稱它不是。

我不認為我理解他們所做的區別。

解釋是什麼?




這將為您提供有關Java如何工作的一些見解,以至於在您下次討論Java通過引用傳遞或通過值傳遞時,您只需微笑:-)

第一步請從腦海中刪除以'p'“_ _ _ _ _ _ _”開頭的單詞,特別是如果您來自其他編程語言。 Java和'p'不能寫在同一本書,論壇甚至txt中。

第二步記住,當你將一個Object傳遞給一個方法時,你傳遞的是Object引用,而不是Object本身。

  • 學生 :師父,這是否意味著Java是傳遞參考?
  • 師父 :蚱蜢,沒有。

現在想想Object的引用/變量是什麼/是什麼:

  1. 變量保存位,告訴JVM如何到達內存中引用的Object(Heap)。
  2. 將參數傳遞給方法時, 您不傳遞引用變量,而是傳遞引用變量中的位副本 。 像這樣:3bad086a。 3bad086a表示獲取傳遞對象的方法。
  3. 所以你只是傳遞3bad086a它是引用的值。
  4. 您傳遞的是引用的值而不是引用本身(而不是對象)。
  5. 該值實際上是COPIED並賦予方法

在下面(請不要嘗試編譯/執行此...):

1. Person person;
2. person = new Person("Tom");
3. changeName(person);
4.
5. //I didn't use Person person below as an argument to be nice
6. static void changeName(Person anotherReferenceToTheSamePersonObject) {
7.     anotherReferenceToTheSamePersonObject.setName("Jerry");
8. }

怎麼了?

  • 變量person在第1行中創建,並且在開頭時為null。
  • 在第2行創建一個新的Person對象,存儲在內存中,並為變量person提供對Person對象的引用。 那就是它的地址。 比方說3bad086a。
  • 保存Object對像地址的變量person將傳遞給第3行中的函數。
  • 在第4行,你可以聽到沉默的聲音
  • 檢查第5行的評論
  • 創建一個方法局部變量 - anotherReferenceToTheSamePersonObject - 然後在#6行中產生魔力:
    • 變量/引用被逐位複制並傳遞給函數內的anotherReferenceToTheSamePersonObject
    • 沒有創建Person的新實例。
    • person ”和“ anotherReferenceToTheSamePersonObject ”都保持相同的3bad086a值。
    • 不要試試這個,但是人== anotherReferenceToTheSamePersonObject會是真的。
    • 兩個變量都具有引用的IDENTICAL COPIES,它們都引用相同的Person對象,堆上的SAME對象而不是COPY。

一張圖片勝過千言萬語:

請注意,anotherReferenceToTheSamePersonObject箭頭指向Object而不是指向變量person!

如果你沒有得到它,那麼只要相信我,並記住最好說Java是通過值傳遞的 。 那麼, 通過參考值傳遞 。 哦,更好的是傳遞變量值的副本! ;)

現在可以隨意討厭我,但請注意,在討論方法參數時,傳遞原始數據類型和對象之間沒有區別

您總是傳遞參考值的位副本!

  • 如果它是原始數據類型,則這些位將包含原始數據類型本身的值。
  • 如果它是一個Object,那麼這些位將包含告訴JVM如何到達Object的地址值。

Java是按值傳遞的,因為在方法中你可以根據需要修改引用的對象,但無論你怎麼努力,你都永遠無法修改將繼續引用的傳遞變量(不是p _ _ _ _ _ _ _)同樣的對象無論如何!

上面的changeName函數永遠不能修改傳遞的引用的實際內容(位值)。 換句話說,changeName不能使Person人引用另一個Object。

當然,你可以縮短它,只是說Java是值得傳遞的!




Java按值傳遞引用。

因此,您無法更改傳入的引用。




為了顯示對比,請比較以下C++Java段:

在C ++中: 注意:錯誤的代碼 - 內存洩漏! 但它證明了這一點。

void cppMethod(int val, int &ref, Dog obj, Dog &objRef, Dog *objPtr, Dog *&objPtrRef)
{
    val = 7; // Modifies the copy
    ref = 7; // Modifies the original variable
    obj.SetName("obj"); // Modifies the copy of Dog passed
    objRef.SetName("objRef"); // Modifies the original Dog passed
    objPtr->SetName("objPtr"); // Modifies the original Dog pointed to 
                               // by the copy of the pointer passed.
    objPtr = new Dog("newObjPtr");  // Modifies the copy of the pointer, 
                                   // leaving the original object alone.
    objPtrRef->SetName("objRefPtr"); // Modifies the original Dog pointed to 
                                    // by the original pointer passed. 
    objPtrRef = new Dog("newObjPtrRef"); // Modifies the original pointer passed
}

int main()
{
    int a = 0;
    int b = 0;
    Dog d0 = Dog("d0");
    Dog d1 = Dog("d1");
    Dog *d2 = new Dog("d2");
    Dog *d3 = new Dog("d3");
    cppMethod(a, b, d0, d1, d2, d3);
    // a is still set to 0
    // b is now set to 7
    // d0 still have name "d0"
    // d1 now has name "objRef"
    // d2 now has name "objPtr"
    // d3 now has name "newObjPtrRef"
}

在Java中

public static void javaMethod(int val, Dog objPtr)
{
   val = 7; // Modifies the copy
   objPtr.SetName("objPtr") // Modifies the original Dog pointed to 
                            // by the copy of the pointer passed.
   objPtr = new Dog("newObjPtr");  // Modifies the copy of the pointer, 
                                  // leaving the original object alone.
}

public static void main()
{
    int a = 0;
    Dog d0 = new Dog("d0");
    javaMethod(a, d0);
    // a is still set to 0
    // d0 now has name "objPtr"
}

Java只有兩種類型的傳遞:內置類型的值,以及對像類型的指針值。




基本上,重新分配Object參數不會影響參數,例如,

private void foo(Object bar) {
    bar = null;
}

public static void main(String[] args) {
    String baz = "Hah!";
    foo(baz);
    System.out.println(baz);
}

將打印"Hah!"而不是null。這個工作的原因是因為bar是一個值的副本baz,它只是一個引用"Hah!" 。如果它是實際的引用本身,那麼foo將重新定義baznull




的問題的關鍵是,字參考在表述“通過引用傳遞”是指某物從字的通常含義完全不同的參考在Java中。

通常在Java 引用中表示對對象的引用。但是,編程語言理論中通過引用/值傳遞的技術術語正在討論對存儲變量的存儲器單元引用,這是完全不同的。




無論您使用何種語言,引用始終是表示的值。

獲取框外視圖,讓我們看看Assembly或一些低級內存管理。在CPU級別,如果將任何內容寫入內存或其中一個CPU寄存器,則對任何內容的引用會立即變為。(這就是為什麼指針是一個很好的定義。它是一個值,它同時具有目的)。

內存中的數據有一個位置,在該位置有一個值(字節,字,等等)。在Assembly中,我們有一個方便的解決方案,可以為某個位置(也就是變量)提供一個名稱,但是在編譯代碼時,彙編程序只是將Name替換為指定位置,就像瀏覽器用IP地址替換域名一樣。

在核心的情況下,技術上不可能在不表示任何語言的情況下將引用傳遞給任何語言(當它立即變為值時)。

比方說,我們有一個變量富,它的位置是在內存中的第47字節,它的是5,我們有另一個變量Ref2Foo這是在第223次內存字節,並且它的值是47.這Ref2Foo可能是一個技術性的變量,不是由程序明確創建的。如果您只看5和47而沒有任何其他信息,您將只看到兩個。如果您將它們用作參考,那麼5我們必須前往:

(Name)[Location] -> [Value at the Location]
---------------------
(Ref2Foo)[223]  -> 47
(Foo)[47]       -> 5

這就是跳轉表的工作原理。

如果我們想用Foo的值調用方法/函數/過程,有幾種可能的方法將變量傳遞給方法,具體取決於語言及其幾種方法調用模式:

  1. 5被複製到其中一個CPU寄存器(即EAX)。
  2. 5獲得PUSHd到堆棧。
  3. 47被複製到其中一個CPU寄存器
  4. 47推到堆棧。
  5. 223被複製到其中一個CPU寄存器。
  6. 223獲取PUSHd到堆棧。

在每個情況下,都創建了一個值 - 現有值的副本 - 現在由接收方法來處理它。當你在方法中寫入“Foo”時,它要么從EAX中讀出,要么自動解除引用,或者雙重解引用,這個過程取決於語言的工作方式和/或Foo的類型。這是開發人員隱藏的,直到她繞過解除引用過程。因此,引用是表示的,因為引用是必須處理的值(在語言級別)。

現在我們已經將Foo傳遞給了方法:

  • 在情況1.和2.如果您更改Foo(Foo = 9)它只影響本地範圍,因為您有一個值的副本。從方法內部我們甚至無法確定原始Foo所在的內存位置。
  • 如果您使用默認語言結構並更改Foo(Foo = 11),它可以全局更改Foo(取決於語言,即Java或類似Pascal的procedure findMin(x, y, z: integer; var m : integer);)。但是,如果該語言允許您繞過取消引用過程,您可以更改47,比方說49。在那一點上,如果你讀它,Foo似乎已被改變了,因為你已經改變了它的本地指針。如果你要在方法(Foo = 12)中修改這個Foo,你可能會搞砸程序的執行(又名.segfault),因為你會寫一個不同於預期的內存,你甚至可以修改一個注定要保存可執行文件的區域程序和寫入它將修改運行代碼(Foo現在不在47)。但是Foo的價值47沒有全局更改,只有方法內部的一個,因為47也是方法的副本。
  • 在第5和第6例中。如果你223在方法內修改它會產生與3.或4中相同的混亂。(一個指針指向一個現在錯誤的值,它再次用作指針)但這仍然是一個本地問題,因為223被複製了。但是,如果您能夠取消引用Ref2Foo(即223),達到並修改指向的值47,比如說,49它將全局影響Foo ,因為在這種情況下,方法得到了一個副本,223但引用的47只存在一次,並且更改了對49每一個會導致Ref2Foo雙解引用到一個錯誤的值。

對無關緊要的細節進行挑剔,即使是通過引用傳遞的語言也會將值傳遞給函數,但這些函數知道它們必須將它用於解除引用目的。這個傳遞參考值只是程序員隱藏的,因為它實際上是無用的,術語只是通過引用傳遞

嚴格的pass-by-value也是無用的,這意味著每次調用一個以數組為參數的方法時都必須複製一個100 MB的數組,因此Java不能嚴格按值傳遞。每種語言都會傳遞對這個巨大數組的引用(作為一個值),並且如果該數組可以在方法內部進行本地更改,或者允許該方法(如Java那樣)全局修改數組,則採用寫時復制機制(來自調用者的視圖)和一些語言允許修改引用本身的值。

因此,簡而言之,在Java自己的術語中,Java是值傳遞,其中可以是:實數值或作為引用表示的




據我所知,Java只知道按值調用。這意味著對於原始數據類型,您將使用副本,對於對象,您將使用對象的引用副本。不過我覺得有一些陷阱; 例如,這不起作用:

public static void swap(StringBuffer s1, StringBuffer s2) {
    StringBuffer temp = s1;
    s1 = s2;
    s2 = temp;
}


public static void main(String[] args) {
    StringBuffer s1 = new StringBuffer("Hello");
    StringBuffer s2 = new StringBuffer("World");
    swap(s1, s2);
    System.out.println(s1);
    System.out.println(s2);
}

這將填充Hello World而不是World Hello,因為在交換函數中,您使用的copys對main中的引用沒有影響。但是如果你的對像不是不可變的,你可以改變它,例如:

public static void appendWorld(StringBuffer s1) {
    s1.append(" World");
}

public static void main(String[] args) {
    StringBuffer s = new StringBuffer("Hello");
    appendWorld(s);
    System.out.println(s);
}

這將在命令行上填充Hello World。如果將StringBuffer更改為String,它將只生成Hello,因為String是不可變的。例如:

public static void appendWorld(String s){
    s = s+" World";
}

public static void main(String[] args) {
    String s = new String("Hello");
    appendWorld(s);
    System.out.println(s);
}

但是你可以為這樣的String創建一個包裝器,這樣就可以將它與字符串一起使用:

class StringWrapper {
    public String value;

    public StringWrapper(String value) {
        this.value = value;
    }
}

public static void appendWorld(StringWrapper s){
    s.value = s.value +" World";
}

public static void main(String[] args) {
    StringWrapper s = new StringWrapper("Hello");
    appendWorld(s);
    System.out.println(s.value);
}

編輯:我相信這也是使用StringBuffer的原因,當涉及到“添加”兩個字符串,因為你可以修改原始對象,你不能用像String這樣的不可變對象。




讓我試著借助四個例子來解釋我的理解。Java是按值傳遞的,而不是按引用傳遞

/ **

通過價值

在Java中,所有參數都按值傳遞,即調用者無法分配方法參數。

* /

例1:

public class PassByValueString {
    public static void main(String[] args) {
        new PassByValueString().caller();
    }

    public void caller() {
        String value = "Nikhil";
        boolean valueflag = false;
        String output = method(value, valueflag);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'value' and 'valueflag'
         */
        System.out.println("output : " + output);
        System.out.println("value : " + value);
        System.out.println("valueflag : " + valueflag);

    }

    public String method(String value, boolean valueflag) {
        value = "Anand";
        valueflag = true;
        return "output";
    }
}

結果

output : output
value : Nikhil
valueflag : false

例2:

/ ** * *通過價值* * /

public class PassByValueNewString {
    public static void main(String[] args) {
        new PassByValueNewString().caller();
    }

    public void caller() {
        String value = new String("Nikhil");
        boolean valueflag = false;
        String output = method(value, valueflag);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'value' and 'valueflag'
         */
        System.out.println("output : " + output);
        System.out.println("value : " + value);
        System.out.println("valueflag : " + valueflag);

    }

    public String method(String value, boolean valueflag) {
        value = "Anand";
        valueflag = true;
        return "output";
    }
}

結果

output : output
value : Nikhil
valueflag : false

例3:

/ **這種'通過價值傳遞'有'通過參考傳遞'的感覺

有些人說原始類型和'String'是'按值傳遞'而對像是'按引用傳遞'。

但是從這個例子中,我們可以理解它只是通過值傳遞,記住這裡我們將引用作為值傳遞。ie:引用按值傳遞。這就是為什麼能夠在局部範圍之後改變並仍然適用的原因。但我們無法改變原始範圍之外的實際參考。這意味著下一個PassByValueObjectCase2示例。

* /

public class PassByValueObjectCase1 {

    private class Student {
        int id;
        String name;
        public Student() {
        }
        public Student(int id, String name) {
            super();
            this.id = id;
            this.name = name;
        }
        public int getId() {
            return id;
        }
        public void setId(int id) {
            this.id = id;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        @Override
        public String toString() {
            return "Student [id=" + id + ", name=" + name + "]";
        }
    }

    public static void main(String[] args) {
        new PassByValueObjectCase1().caller();
    }

    public void caller() {
        Student student = new Student(10, "Nikhil");
        String output = method(student);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'student'
         */
        System.out.println("output : " + output);
        System.out.println("student : " + student);
    }

    public String method(Student student) {
        student.setName("Anand");
        return "output";
    }
}

結果

output : output
student : Student [id=10, name=Anand]

例4:

/ **

除了Example3(PassByValueObjectCase1.java)中提到的內容之外,我們無法更改原始範圍之外的實際引用。“

注意:我沒有粘貼代碼private class Student。類定義Student與Example3相同。

* /

public class PassByValueObjectCase2 {

    public static void main(String[] args) {
        new PassByValueObjectCase2().caller();
    }

    public void caller() {
        // student has the actual reference to a Student object created
        // can we change this actual reference outside the local scope? Let's see
        Student student = new Student(10, "Nikhil");
        String output = method(student);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'student'
         */
        System.out.println("output : " + output);
        System.out.println("student : " + student); // Will it print Nikhil or Anand?
    }

    public String method(Student student) {
        student = new Student(20, "Anand");
        return "output";
    }

}

結果

output : output
student : Student [id=10, name=Nikhil]



Java是按值調用的。

這個怎麼運作

  • 您總是傳遞參考值的位副本!

  • 如果它是原始數據類型,則這些位包含原始數據類型本身的值,這就是為什麼如果我們更改方法內的header的值,那麼它不會反映外部的更改。

  • 如果它是一個像Foo foo = new Foo()這樣的對像數據類型,那麼在這種情況下,對像地址的副本會像文件快捷方式一樣傳遞,假設我們在C:\ desktop中有一個文本文件abc.txt,並假設我們設置了快捷方式相同的文件,並把它放在C:\ desktop \ abc-shortcut中,所以當你從C:\ desktop \ abc.txt訪問文件並寫入''並關閉文件時再次從快捷方式打開文件然後你write '是程序員學習的最大的在線社區'然後總文件更改將是'是程序員學習的最大的在線社區'這意味著從打開文件的位置開始無關緊要,每次我們訪問同一個文件時,我們都可以假設Foo為文件,並假設foo存儲在123hd7h(原始地址如C:\ desktop \ abc.txt)地址和234jdid(複製地址如C:\ desktop \ abc-shortcut,其中實際上包含了文件的原始地址)。所以為了更好地理解製作快捷方式文件和感覺...




區別,或者也許只是我記憶中的方式,因為我曾經與原始海報的印象相同:Java總是按價值傳遞。Java中的所有對象(在Java中,除了基元之外的任何東西)都是引用。這些引用按值傳遞。




Java只傳遞值。一個非常簡單的例子來驗證這一點。

public void test() {
    MyClass obj = null;
    init(obj);
    //After calling init method, obj still points to null
    //this is because obj is passed as value and not as reference.
}
private void init(MyClass objVar) {
    objVar = new MyClass();
}



我一直認為它是“通過副本”。它是值的原始或引用的副本。如果它是原語,則它是作為值的位的副本,如果它是Object,則它是引用的副本。

public class PassByCopy{
    public static void changeName(Dog d){
        d.name = "Fido";
    }
    public static void main(String[] args){
        Dog d = new Dog("Maxx");
        System.out.println("name= "+ d.name);
        changeName(d);
        System.out.println("name= "+ d.name);
    }
}
class Dog{
    public String name;
    public Dog(String s){
        this.name = s;
    }
}

java PassByCopy的輸出:

name = Maxx
name = Fido

原始包裝類和字符串是不可變的,因此使用這些類型的任何示例都不會與其他類型/對象相同。




一些帖子的一些更正。

C不支持通過引用傳遞。它總是通過價值。C ++確實支持按引用傳遞,但不是默認值,而且非常危險。

Java中的值是什麼並不重要:對象的原始或地址(粗略),它總是按值傳遞。

如果Java對象“行為”就像通過引用傳遞一樣,那就是可變性的屬性,並且與傳遞機製完全無關。

我不確定為什麼會這麼混亂,也許是因為很多Java“程序員”沒有經過正式培訓,因此不明白內存中究竟發生了什麼?




Java按VALUE傳遞參數,按值傳遞參數。

長話短說:

對於來自C#的人:沒有“out”參數。

對於來自PASCAL的人:沒有“var”參數

這意味著您無法更改對象本身的引用,但您始終可以更改對象的屬性。

解決方法是使用StringBuilder參數String。你總是可以使用數組!




Related