Java "ผ่านการอ้างอิง" หรือ "pass-by-value" หรือไม่?


6578

ผมคิดเสมอว่าเป็น Java ผ่านโดยการอ้างอิง

อย่างไรก็ตามฉันได้เห็นโพสต์บล็อกสองสามตัวอย่าง (ตัวอย่างเช่นบล็อกนี้ ) ที่อ้างว่าไม่ได้

ฉันไม่คิดว่าฉันเข้าใจความแตกต่างที่พวกเขาทำ

คำอธิบายคืออะไร


705
ฉันเชื่อว่าความสับสนในเรื่องนี้เกี่ยวกับความจริงที่ว่าคนต่างมีนิยามของคำว่า "อ้างอิง" ที่แตกต่างกัน ผู้ที่มาจากพื้นหลัง C ++ สมมติว่า "การอ้างอิง" ต้องหมายถึงความหมายใน C ++ คนที่มาจากพื้นหลัง C จะถือว่า "การอ้างอิง" ต้องเหมือนกับ "ตัวชี้" ในภาษาของพวกเขาและอื่น ๆ ไม่ว่าจะถูกต้องหรือไม่ที่จะบอกว่า Java ส่งผ่านการอ้างอิงขึ้นอยู่กับสิ่งที่ "การอ้างอิง" หมายถึง
แรงดึงดูด

119
ฉันพยายามใช้คำศัพท์ที่พบในบทความStrategy Strategyอย่างสม่ำเสมอ มันควรจะตั้งข้อสังเกตว่าแม้ว่าจุดบทความออกข้อกำหนดที่แตกต่างกันมากโดยชุมชนก็เน้นว่าความหมายสำหรับcall-by-valueและcall-by-referenceแตกต่างกันในวิธีที่สำคัญมาก (โดยส่วนตัวแล้วฉันชอบใช้call-by-object-sharingวันนี้มากกว่าcall-by-value[-of-the-reference]เพราะเป็นการอธิบายความหมายในระดับสูงและไม่ได้สร้างความขัดแย้งcall-by-valueซึ่งเป็นการนำไปปฏิบัติ)

59
@ Gravity: คุณสามารถไปและใส่ความคิดเห็นของคุณบนป้ายโฆษณาขนาดใหญ่หรืออะไร? นั่นคือปัญหาทั้งหมดโดยย่อ และมันแสดงให้เห็นว่าสิ่งทั้งหมดนี้เป็นความหมาย ถ้าเราไม่เห็นด้วยกับคำนิยามฐานของการอ้างอิงแล้วเราจะไม่เห็นด้วยกับคำตอบของคำถามนี้ :)
MadConan

29
ฉันคิดว่าความสับสนคือ "ส่งต่อโดยอ้างอิง" กับ "อ้างอิงความหมาย" Java เป็นค่าแบบบายพาสพร้อมซีแมนทิกส์อ้างอิง
spraff

11
@ Gravity ในขณะที่คุณถูกต้องอย่างแน่นอนในคนที่มาจาก C ++ โดยสัญชาตญาณจะมีชุดสัญชาตญาณที่แตกต่างกันเกี่ยวกับคำว่า "อ้างอิง" ผมเองเชื่อว่าร่างกายจะถูกฝังอยู่ใน "โดย" "Pass by" สร้างความสับสนว่าเป็นสิ่งที่แตกต่างอย่างสิ้นเชิงจาก "Passing a" ใน Java ใน C ++ อย่างไรก็ตามมันไม่ได้เรียกขาน ใน C ++ คุณพูดว่า "ที่ผ่านการอ้างอิง" และก็เข้าใจว่ามันจะผ่านการswap(x,y)ทดสอบ

คำตอบ:


5839

Java มักจะผ่านโดยค่า น่าเสียดายที่เมื่อเราส่งค่าของวัตถุเรากำลังส่งการอ้างอิงถึงมัน นี่คือความสับสนสำหรับผู้เริ่มต้น

มันจะเป็นเช่นนี้:

public static void main(String[] args) {
    Dog aDog = new Dog("Max");
    Dog oldDog = aDog;

    // we pass the object to foo
    foo(aDog);
    // aDog variable is still pointing to the "Max" dog when foo(...) returns
    aDog.getName().equals("Max"); // true
    aDog.getName().equals("Fifi"); // false
    aDog == oldDog; // true
}

public static void foo(Dog d) {
    d.getName().equals("Max"); // true
    // change d inside of foo() to point to a new Dog instance "Fifi"
    d = new Dog("Fifi");
    d.getName().equals("Fifi"); // true
}

ในตัวอย่างข้างต้นจะยังคงกลับมาaDog.getName() "Max"ค่าaDogภายในmainจะไม่เปลี่ยนแปลงในฟังก์ชั่นที่fooมีการDog "Fifi"อ้างอิงตามวัตถุจะถูกส่งผ่านโดยค่า ถ้ามันถูกส่งผ่านโดยการอ้างอิงแล้วaDog.getName()ในmainจะกลับมาหลังจากที่โทรไป"Fifi"foo

ในทำนองเดียวกัน:

public static void main(String[] args) {
    Dog aDog = new Dog("Max");
    Dog oldDog = aDog;

    foo(aDog);
    // when foo(...) returns, the name of the dog has been changed to "Fifi"
    aDog.getName().equals("Fifi"); // true
    // but it is still the same dog:
    aDog == oldDog; // true
}

public static void foo(Dog d) {
    d.getName().equals("Max"); // true
    // this changes the name of d to be "Fifi"
    d.setName("Fifi");
}

ในตัวอย่างข้างต้นFifiเป็นชื่อของสุนัขหลังจากที่โทรไปเพราะชื่อวัตถุที่ถูกตั้งภายในfoo(aDog) foo(...)การดำเนินการใด ๆ ที่fooดำเนินการdเป็นเช่นนั้นสำหรับวัตถุประสงค์ในทางปฏิบัติทั้งหมดพวกเขาจะดำเนินการaDogแต่มันเป็นไปไม่ได้ที่จะเปลี่ยนค่าของตัวแปรaDogเอง


263
มันทำให้สับสนเล็กน้อยเกี่ยวกับรายละเอียดภายในใช่หรือไม่ ไม่มีความแตกต่างทางแนวคิดระหว่าง 'การส่งการอ้างอิง' และ 'การส่งค่าของการอ้างอิง' โดยสมมติว่าคุณหมายถึง 'ค่าของตัวชี้ภายในไปยังวัตถุ'
izb

383
แต่มีความแตกต่างเล็กน้อย ดูตัวอย่างแรก ถ้ามันผ่านการอ้างอิงหมดจด aDog.name จะเป็น "Fifi" ไม่ใช่ - การอ้างอิงที่คุณได้รับคือการอ้างอิงค่าที่หากการเขียนทับจะถูกกู้คืนเมื่อออกจากฟังก์ชัน
erlando

300
@Lorenzo: ไม่มีใน Java ทุกอย่างถูกส่งผ่านโดยค่า จะถูกส่งผ่านค่าพื้นฐานและการอ้างอิงวัตถุจะถูกส่งผ่านตามค่า วัตถุนั้นไม่เคยถูกส่งไปยังเมธอด แต่วัตถุนั้นจะอยู่ในฮีปเสมอและจะมีการอ้างอิงถึงออบเจ็กต์เท่านั้นถึงวิธีการ
Esko Luontola

294
ความพยายามของฉันเป็นวิธีที่ดีในการมองเห็นภาพวัตถุผ่าน: ลองจินตนาการถึงบอลลูน การเรียก fxn นั้นเหมือนกับการผูกสตริงที่สองกับบอลลูนและส่งสายไปยัง fxn parameter = new Balloon () จะตัดสตริงนั้นและสร้างบอลลูนใหม่ (แต่สิ่งนี้จะไม่มีผลกับบอลลูนต้นฉบับ) parameter.pop () จะยังคงปรากฏแม้ว่าเนื่องจากตามสตริงไปยังบอลลูนเดิม Java คือการส่งผ่านค่า แต่ค่าที่ผ่านไม่ลึกมันอยู่ในระดับสูงสุดเช่นดั้งเดิมหรือตัวชี้ อย่าสับสนว่ามีการส่งผ่านค่าที่ลึกซึ่งวัตถุถูกโคลนและส่งผ่านทั้งหมด
dhackner

150
สิ่งที่สับสนคือการอ้างอิงวัตถุเป็นตัวชี้ ในจุดเริ่มต้นอาทิตย์เรียกพวกเขาชี้ จากนั้นฝ่ายการตลาดก็แจ้งว่า "ตัวชี้" เป็นคำที่ไม่ดี แต่คุณยังคงเห็นการตั้งชื่อ "ถูกต้อง" ใน NullPointerException
ศ. Falken ผิดสัญญา

3035

ฉันเพิ่งสังเกตเห็นว่าคุณอ้างถึงบทความของฉัน

Java Spec บอกว่าทุกอย่างใน Java เป็นรหัสผ่าน ไม่มีสิ่งเช่น "Pass-by-Reference" ใน Java

กุญแจสำคัญในการทำความเข้าใจนี้เป็นสิ่งที่ชอบ

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มันชี้ไปที่ ( Dogวัตถุที่อยู่ 42)
    • ที่Dog(ที่อยู่ 42) ขอให้เปลี่ยนชื่อเป็น Max
  • ที่บรรทัด "BBB"
    • ใหม่Dogถูกสร้างขึ้น สมมติว่าเขาอยู่ที่ที่อยู่ 74
    • เรากำหนดพารามิเตอร์someDogให้กับ 74
  • ที่บรรทัด "CCC"
    • someDog ถูกตามด้วยDogมันชี้ไปที่ ( Dogวัตถุที่อยู่ 74)
    • ที่Dog(ที่อยู่ 74) ขอให้เปลี่ยนชื่อเป็น Rowlf
  • จากนั้นเรากลับมา

ทีนี้ลองคิดดูว่าเกิดอะไรขึ้นนอกวิธีการ:

ไม่myDogเปลี่ยนแปลง?

มีกุญแจ

โปรดจำไว้ว่าmyDogเป็นตัวชี้และไม่ใช่ของจริงDogคำตอบคือไม่ myDogยังคงมีค่า 42; มันยังคงชี้ไปที่ต้นฉบับDog(แต่โปรดทราบว่าเนื่องจากบรรทัด "AAA" ชื่อของมันคือ "สูงสุด" - ยังคงเป็นค่าเดียวกัน Dog; myDogค่าของDog จึงไม่เปลี่ยนแปลง)

มันสมบูรณ์ในการติดตามที่อยู่และเปลี่ยนสิ่งที่อยู่ท้ายสุด ที่ไม่เปลี่ยนแปลงตัวแปรอย่างไรก็ตาม

Java ทำงานเหมือนกับ C คุณสามารถกำหนดตัวชี้ส่งตัวชี้ไปยังวิธีการติดตามตัวชี้ในวิธีการและเปลี่ยนข้อมูลที่ชี้ไป อย่างไรก็ตามคุณไม่สามารถเปลี่ยนจุดที่ตัวชี้นั้นได้

ใน C ++, Ada, Pascal และภาษาอื่น ๆ ที่สนับสนุนการอ้างอิงแบบอ้างอิงคุณสามารถเปลี่ยนตัวแปรที่ส่งผ่านได้

หาก Java มีซีแมนทิกส์แบบอ้างอิงอ้างอิงfooวิธีการที่เรากำหนดไว้ด้านบนจะมีการเปลี่ยนแปลงโดยmyDogชี้ไปที่ตำแหน่งที่กำหนดsomeDogบนบรรทัด BBB

คิดว่าพารามิเตอร์อ้างอิงเป็นนามแฝงสำหรับตัวแปรที่ส่งผ่านเมื่อกำหนดนามแฝงนั้นแล้วจึงเป็นตัวแปรที่ส่งผ่าน


164
นี่คือเหตุผลที่การละเว้นทั่วไป "Java ไม่มีตัวชี้" ทำให้เข้าใจผิด
Beska

134
คุณผิด "โปรดจำไว้ว่า myDog เป็นตัวชี้และไม่ใช่สุนัขจริงคำตอบคือไม่ myDog ยังคงมีค่า 42 แต่ยังคงชี้ไปที่ Dog ดั้งเดิม" myDog มีค่า 42 แต่อาร์กิวเมนต์ชื่อตอนนี้มี "Max" มากกว่า "Rover" ใน // // AAA line
Özgür

180
คิดแบบนี้ บางคนมีที่อยู่ของ Ann Arbor, MI (บ้านเกิดของฉัน GO BLUE!) บนกระดาษใบหนึ่งชื่อ "annArborLocation" คุณคัดลอกมันลงบนแผ่นกระดาษที่เรียกว่า "myDestination" คุณสามารถขับรถไปที่ "myDestination" และปลูกต้นไม้ คุณอาจเปลี่ยนบางสิ่งบางอย่างเกี่ยวกับเมือง ณ สถานที่นั้น แต่มันไม่เปลี่ยน LAT / LON ที่เขียนบนกระดาษทั้งสอง คุณสามารถเปลี่ยน LAT / LON ใน "myDestination" แต่จะไม่เปลี่ยน "annArborLocation" มันช่วยได้ไหม
Scott Stanchfield

43
@Scott Stanchfield: ฉันอ่านบทความของคุณเมื่อประมาณหนึ่งปีที่ผ่านมาและมันช่วยล้างสิ่งต่าง ๆ ให้ฉัน ขอบคุณ! ฉันขอแนะนำเล็กน้อยเพิ่มเติมได้หรือไม่: คุณควรพูดถึงว่ามีคำเฉพาะที่อธิบายรูปแบบของ "การโทรตามมูลค่าโดยที่การอ้างอิงนั้นเป็นค่า" ซึ่งคิดค้นโดยบาร์บาร่าลิสคอฟเพื่ออธิบายกลยุทธ์การประเมินภาษา CLU ของเธอ ในปี 1974 เพื่อหลีกเลี่ยงความสับสนเช่นที่อยู่บทความของคุณ: โทรโดยใช้งานร่วมกัน (บางครั้งเรียกว่าโทรโดยใช้วัตถุร่วมกันหรือเพียงแค่เรียกโดยวัตถุ ) ซึ่งอธิบายความหมายได้อย่างสมบูรณ์
Jörg W Mittag

29
@Gevorg - ภาษา C / C ++ ไม่ได้เป็นเจ้าของแนวคิดของ "พอยน์เตอร์" มีภาษาอื่นที่ใช้พอยน์เตอร์ แต่ไม่อนุญาตการใช้พอยน์เตอร์ประเภทเดียวกันกับที่ C / C ++ อนุญาต Java มีพอยน์เตอร์ พวกเขากำลังป้องกันความเสียหาย
Scott Stanchfield

1740

Java ส่งค่าอาร์กิวเมนต์ตามค่าเสมอไม่ใช่อ้างอิง


ให้ฉันอธิบายสิ่งนี้ผ่านตัวอย่าง :

public class Main {

     public static void main(String[] args) {
          Foo f = new Foo("f");
          changeReference(f); // It won't change the reference!
          modifyReference(f); // It will modify the object that the reference variable "f" refers to!
     }

     public static void changeReference(Foo a) {
          Foo b = new Foo("b");
          a = b;
     }

     public static void modifyReference(Foo c) {
          c.setAttribute("c");
     }

}

ฉันจะอธิบายในขั้นตอนนี้:

  1. ประกาศอ้างอิงชื่อfประเภทFooและกำหนดวัตถุใหม่ชนิดที่มีแอตทริบิวต์Foo"f"

    Foo f = new Foo("f");

    ป้อนคำอธิบายรูปภาพที่นี่

  2. จากด้านข้างวิธีการอ้างอิงจากประเภทFooที่มีชื่อถูกประกาศและจะได้รับมอบหมายแรกanull

    public static void changeReference(Foo a)

    ป้อนคำอธิบายรูปภาพที่นี่

  3. ในขณะที่คุณเรียกใช้เมธอดchangeReferenceนั้นการอ้างอิงaจะถูกกำหนดให้กับออบเจ็กต์ที่ถูกส่งเป็นอาร์กิวเมนต์

    changeReference(f);

    ป้อนคำอธิบายรูปภาพที่นี่

  4. ประกาศอ้างอิงชื่อbประเภทFooและกำหนดวัตถุใหม่ชนิดที่มีแอตทริบิวต์Foo"b"

    Foo b = new Foo("b");

    ป้อนคำอธิบายรูปภาพที่นี่

  5. a = bทำให้งานใหม่ที่จะอ้างอิงa, ไม่ได้ ของวัตถุที่มีแอตทริบิวต์ของมันคือf"b"

    ป้อนคำอธิบายรูปภาพที่นี่

  6. ในขณะที่คุณเรียกmodifyReference(Foo c)วิธีการอ้างอิงจะถูกสร้างขึ้นและได้รับมอบหมายวัตถุที่มีแอตทริบิวต์c"f"

    ป้อนคำอธิบายรูปภาพที่นี่

  7. c.setAttribute("c");จะเปลี่ยนแอตทริบิวต์ของวัตถุที่อ้างอิงถึงcจุดนั้นและเป็นวัตถุเดียวกันที่fชี้ไปยังวัตถุนั้น

    ป้อนคำอธิบายรูปภาพที่นี่

ฉันหวังว่าคุณจะเข้าใจในขณะนี้ว่าการส่งผ่านวัตถุเป็นอาร์กิวเมนต์ทำงานใน Java :)


82
+1 สิ่งที่ดี ไดอะแกรมที่ดี ฉันพบหน้ารวบรัดที่ดีที่นี่adp-gmbh.ch/php/pass_by_reference.htmlตกลงฉันยอมรับว่ามันเขียนด้วย PHP แต่มันเป็นค่าเริ่มต้นในการทำความเข้าใจความแตกต่างที่ฉันคิดว่าสำคัญ ความต้องการของคุณ)
DaveM

5
@ Eng.Fouad มันเป็นคำอธิบายที่ดี แต่ถ้าaชี้ไปที่วัตถุเดียวกันกับf(และไม่เคยได้รับสำเนาของวัตถุที่fชี้ไป) การเปลี่ยนแปลงใด ๆ กับวัตถุที่ใช้aควรแก้ไขfเช่นกัน (เนื่องจากทั้งคู่ทำงานกับวัตถุเดียวกัน ) ดังนั้นในบางจุดaจะต้องได้รับสำเนาของวัตถุที่fชี้ไป
0x6C38

14
@MrD เมื่อ 'a' ชี้ไปที่วัตถุเดียวกัน 'f' ก็ชี้ไปที่แล้วการเปลี่ยนแปลงใด ๆ ที่เกิดขึ้นกับวัตถุนั้นผ่านทาง 'a' ก็สามารถสังเกตได้ผ่านทาง 'f' แต่ก็ไม่เปลี่ยน 'f' 'f' ยังคงชี้ไปที่วัตถุเดียวกัน คุณสามารถเปลี่ยนวัตถุทั้งหมด แต่คุณไม่สามารถเปลี่ยนสิ่งที่ 'f' ชี้ไป นี่คือปัญหาพื้นฐานที่ด้วยเหตุผลบางคนบางคนก็ไม่สามารถเข้าใจ
Mike Braun

@MikeBraun ... อะไรนะ? ตอนนี้คุณทำให้ฉันงง: S. ไม่ใช่สิ่งที่คุณเพิ่งเขียนตรงกันข้ามกับสิ่งที่ 6 และ 7 แสดงให้เห็น?
เครื่องซักผ้า Evil

6
นี่คือคำอธิบายที่ดีที่สุดที่ฉันได้พบ แล้วสถานการณ์ขั้นพื้นฐานล่ะ? ตัวอย่างเช่นอาร์กิวเมนต์ต้องการประเภท int แต่ยังคงผ่านการคัดลอกตัวแปร int ไปยังอาร์กิวเมนต์หรือไม่
allenwang

732

สิ่งนี้จะให้ข้อมูลเชิงลึกเกี่ยวกับวิธีที่ Java ใช้งานได้จริงจนถึงจุดที่ในการอภิปรายครั้งต่อไปเกี่ยวกับ Java การส่งต่อโดยการอ้างอิงหรือการส่งผ่านค่าคุณจะยิ้มได้ :-)

ขั้นตอนที่หนึ่งโปรดลบออกจากใจของคุณว่าคำที่ขึ้นต้นด้วย 'p' "_ _ _ _ _ _" โดยเฉพาะอย่างยิ่งถ้าคุณมาจากภาษาโปรแกรมอื่น Java และ 'p' ไม่สามารถเขียนในหนังสือฟอรัมเดียวกันหรือแม้แต่ txt

ขั้นตอนที่สองจำไว้ว่าเมื่อคุณส่ง Object ไปยังเมธอดคุณจะผ่านการอ้างอิง Object ไม่ใช่ตัว Object เอง

  • นักเรียน : ปรมาจารย์นี่หมายความว่า Java เป็นการอ้างอิงแบบส่งผ่านหรือไม่
  • Master : ตั๊กแตนหมายเลข

ตอนนี้คิดว่าการอ้างอิง / ตัวแปรของ Object ทำอะไร / คือ:

  1. ตัวแปรถือบิตที่บอก JVM ว่าจะไปยัง Object ที่อ้างอิงในหน่วยความจำ (Heap) ได้อย่างไร
  2. เมื่อผ่านการขัดแย้งที่จะเป็นวิธีการที่คุณยังไม่ได้ผ่านตัวแปรอ้างอิง แต่คัดลอกบิตในตัวแปรอ้างอิง บางสิ่งเช่นนี้: 3bad086a 3bad086a แสดงถึงวิธีการไปยังวัตถุที่ส่งผ่าน
  3. คุณแค่ผ่าน 3bad086a ว่ามันคือคุณค่าของการอ้างอิง
  4. คุณกำลังส่งค่าของการอ้างอิงไม่ใช่การอ้างอิงเอง (ไม่ใช่วัตถุ)
  5. ค่านี้จะถูกคัดลอกจริงและได้รับกับวิธีการ

ในสิ่งต่อไปนี้ (โปรดอย่าพยายามรวบรวม / ดำเนินการสิ่งนี้ ... ):

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. }

เกิดอะไรขึ้น?

  • บุคคลตัวแปรถูกสร้างในบรรทัด # 1 และเป็นค่าเริ่มต้น
  • วัตถุบุคคลใหม่ถูกสร้างขึ้นในบรรทัดที่ 2 เก็บไว้ในหน่วยความจำและบุคคลตัวแปรจะได้รับการอ้างอิงถึงวัตถุบุคคล นั่นคือที่อยู่ของมัน สมมุติว่า 3bad086a
  • บุคคลตัวแปรที่ถือที่อยู่ของวัตถุจะถูกส่งผ่านไปยังฟังก์ชันในบรรทัด # 3
  • ในบรรทัดที่ # 4 คุณสามารถฟังเสียงแห่งความเงียบงัน
  • ตรวจสอบความคิดเห็นในบรรทัด # 5
  • เมธอดตัวแปรโลคอล - anotherReferenceToTheSamePersonObject - ถูกสร้างขึ้นและจากนั้นมาเวทย์มนตร์ในบรรทัดที่ 6:
    • ตัวแปร / การอ้างอิงบุคคลที่จะถูกคัดลอกบิตโดยบิตและส่งผ่านไปยังanotherReferenceToTheSamePersonObjectภายในฟังก์ชั่น
    • ไม่มีการสร้างอินสแตนซ์ใหม่ของบุคคล
    • ทั้ง " คน " และ "อีกคนอ้างอิงถึงผู้เป็นที่รัก ThePersonObject " ถือค่าเดียวกัน 3bad086a
    • อย่าลองทำแบบนี้ยกเว้นคน == anotherReferenceToTheSamePersonObject จะเป็นจริง
    • ตัวแปรทั้งสองมีสำเนาอ้างอิงของการอ้างอิงและทั้งคู่อ้างถึงวัตถุบุคคลเดียวกันวัตถุเดียวกันในกองและไม่คัดลอก

ภาพที่มีค่าพันคำ:

ผ่านมูลค่า

โปรดทราบว่าลูกศร AnotherReferenceToTheSamePersonObject นั้นพุ่งเข้าหาวัตถุและไม่ใช่ไปที่บุคคลตัวแปร!

หากคุณไม่ได้รับมันเพียงแค่เชื่อฉันและจำไว้ว่าเป็นการดีกว่าที่จะบอกว่าJava มีค่าใช้จ่าย ดีผ่านค่าอ้างอิง โอ้ดีกว่าคือการส่งผ่านค่าตัวแปร! ;)

ตอนนี้อย่าลังเลที่จะเกลียดฉัน แต่โปรดทราบว่าเนื่องจากสิ่งนี้ไม่มีความแตกต่างระหว่างการส่งชนิดข้อมูลดั้งเดิมและวัตถุเมื่อพูดถึงอาร์กิวเมนต์ของเมธอด

คุณส่งสำเนาบิตของค่าอ้างอิงเสมอ!

  • หากเป็นชนิดข้อมูลดั้งเดิมบิตเหล่านี้จะมีค่าของชนิดข้อมูลดั้งเดิมเอง
  • หากเป็นวัตถุบิตจะมีค่าของที่อยู่ที่แจ้งให้ JVM ทราบว่าจะไปยังวัตถุได้อย่างไร

Java เป็นแบบ pass-by-value เพราะภายในวิธีที่คุณสามารถแก้ไข Object ที่อ้างอิงได้มากเท่าที่คุณต้องการ แต่ไม่ว่าคุณจะพยายามมากแค่ไหนคุณจะไม่สามารถแก้ไขตัวแปรที่ส่งผ่านซึ่งจะทำการอ้างอิงต่อไป (ไม่ใช่ p _ _ _ _ _ _ _) วัตถุเดียวกันไม่ว่าจะเกิดอะไรขึ้น!


ฟังก์ชัน changeName ข้างต้นจะไม่สามารถแก้ไขเนื้อหาจริง (ค่าบิต) ของการอ้างอิงที่ส่งผ่านได้ ในคำอื่น changeName ไม่สามารถทำให้บุคคลบุคคลอ้างถึงวัตถุอื่น


แน่นอนคุณสามารถตัดสั้นและเพียงบอกว่า Java เป็นค่าผ่าน!


5
คุณชี้หมายถึง? .. ถ้าผมได้รับมันอย่างถูกต้องในpublic void foo(Car car){ ... }, carอยู่ภายในfooและมีสถานที่ตั้งของกองวัตถุ? ดังนั้นถ้าฉันเปลี่ยนcarค่าโดยcar = new Car()มันจะชี้ไปที่วัตถุที่แตกต่างในกอง? และถ้าฉันเปลี่ยนค่าcarของคุณสมบัติโดยcar.Color = "Red"วัตถุในฮีปที่ชี้โดยcarจะถูกแก้ไข นอกจากนี้มันจะเหมือนกันใน C #? กรุณาตอบกลับ! ขอบคุณ!
dpp

11
@domanokz คุณกำลังฆ่าฉันโปรดอย่าพูดคำนั้นอีกเลย! ;) โปรดทราบว่าฉันสามารถตอบคำถามนี้ได้โดยไม่ต้องพูดว่า 'อ้างอิง' เช่นกัน มันเป็นปัญหาคำศัพท์และทำให้แย่ลง ฉันกับชาวสกอตมีมุมมองที่ต่างออกไปในเรื่องนี้โชคไม่ดี ฉันคิดว่าคุณเข้าใจวิธีการใช้งานจาวาในตอนนี้คุณสามารถเรียกมันผ่านค่าต่อการแบ่งปันโดยวัตถุการคัดลอกค่าตัวแปรหรือรู้สึกอิสระที่จะทำสิ่งอื่น! ฉันไม่สนใจตราบใดที่คุณมีวิธีการทำงานและสิ่งที่อยู่ในประเภทของ Object: เพียงแค่ที่อยู่ PO Box! ;)
Marsellus Wallace

9
ดูเหมือนว่าคุณเพิ่งผ่านการอ้างอิงหรือไม่ ฉันจะปกป้องความจริงที่ว่า Java ยังคงเป็นภาษาที่ใช้อ้างอิงแบบคัดลอก ความจริงที่ว่ามันเป็นข้อมูลอ้างอิงที่คัดลอกไม่ได้เปลี่ยนคำศัพท์ ทั้งผู้อ้างอิงยังคงชี้ไปที่วัตถุเดียวกัน นี่เป็นข้อโต้แย้งของคนเจ้าระเบียบ ...
John Strickler

3
ดังนั้นให้วางในบรรทัดทฤษฎี # 9 System.out.println(person.getName());สิ่งที่จะปรากฏขึ้น "Tom" หรือ "Jerry"? นี่คือสิ่งสุดท้ายที่จะช่วยให้ฉันหลีกเลี่ยงความสับสนนี้
TheBrenny

1
"คุณค่าของการอ้างอิง " คือสิ่งที่อธิบายให้ฉันฟังในที่สุด
Alexander Shubert

689

Java จะผ่านเสมอโดยค่าโดยไม่มีข้อยกเว้นที่เคย

แล้วใครจะสับสนกับเรื่องนี้ได้อย่างไรและเชื่อว่า Java นั้นผ่านการอ้างอิงหรือคิดว่าพวกเขามีตัวอย่างของ Java ที่ทำหน้าที่ส่งผ่านการอ้างอิง? ประเด็นสำคัญคือ Java ไม่เคยให้การเข้าถึงคุณค่าโดยตรงของวัตถุในทุกสถานการณ์ การเข้าถึงวัตถุเพียงอย่างเดียวคือผ่านการอ้างอิงไปยังวัตถุนั้น เพราะวัตถุ Java เป็นเสมอเข้าถึงได้ผ่านการอ้างอิงโดยตรงมากกว่ามันเป็นเรื่องธรรมดาที่จะพูดคุยเกี่ยวกับเขตข้อมูลและตัวแปรและข้อโต้แย้งวิธีการในฐานะที่เป็นวัตถุเมื่ออวดพวกเขาเป็นเพียงการอ้างอิงไปยังวัตถุความสับสนเกิดขึ้นจากการเปลี่ยนแปลงนี้ (พูดอย่างไม่ถูกต้อง) ในระบบการตั้งชื่อ

ดังนั้นเมื่อเรียกใช้เมธอด

  • สำหรับอาร์กิวเมนต์ดั้งเดิม ( int,, longฯลฯ ) การส่งผ่านตามค่าเป็นค่าจริงของดั้งเดิม (ตัวอย่างเช่น 3)
  • สำหรับวัตถุผ่านค่าคือค่าของการอ้างอิงไปยังวัตถุ

ดังนั้นถ้าคุณมีdoSomething(foo)และpublic void doSomething(Foo foo) { .. }Foos สองคนได้คัดลอกข้อมูลอ้างอิงที่ชี้ไปยังวัตถุเดียวกัน

โดยธรรมชาติแล้วการส่งผ่านค่าอ้างอิงไปยังวัตถุนั้นดูเหมือนมาก (และไม่สามารถแยกได้ในทางปฏิบัติจาก) ผ่านวัตถุโดยการอ้างอิง


7
เนื่องจากค่าของดั้งเดิมนั้นไม่เปลี่ยนรูป (เช่นสตริง) ความแตกต่างระหว่างสองกรณีจึงไม่เกี่ยวข้องกันจริงๆ
Paŭlo Ebermann

4
เผง สำหรับทุกสิ่งที่คุณสามารถบอกได้ผ่านพฤติกรรม JVM ที่สังเกตได้การอ้างอิงแบบดั้งเดิมอาจถูกส่งผ่านโดยการอ้างอิงและสามารถมีชีวิตอยู่บนกอง พวกเขาทำไม่ได้ แต่นั่นไม่ได้สังเกตได้จริง แต่อย่างใด
แรงดึงดูด

6
ดึกดำบรรพ์ไม่เปลี่ยนรูป? ใหม่ใน Java 7 หรือไม่
user85421

4
พอยน์เตอร์นั้นไม่เปลี่ยนรูปแบบโดยทั่วไปจะไม่สามารถเปลี่ยนแปลงได้ สตริงยังไม่ได้เป็นแบบดั้งเดิมมันเป็นวัตถุ นอกจากนี้โครงสร้างพื้นฐานของ String เป็นอาร์เรย์ที่ไม่แน่นอน สิ่งเดียวที่เปลี่ยนแปลงไม่ได้เกี่ยวกับมันคือความยาวซึ่งเป็นธรรมชาติของอาร์เรย์
kingfrito_5005

3
นี่เป็นอีกคำตอบที่ชี้ให้เห็นถึงความหมายส่วนใหญ่ของการโต้แย้ง คำจำกัดความของการอ้างอิงที่ให้ไว้ในคำตอบนี้จะทำให้ Java "ส่งต่ออ้างอิง" ผู้เขียนยอมรับเป็นอย่างมากในย่อหน้าสุดท้ายโดยระบุว่าเป็น "ความแตกต่างในทางปฏิบัติ" จาก "การส่งต่อโดยอ้างอิง" ฉันสงสัยว่า OP ขอให้มีเพราะความต้องการที่จะเข้าใจการใช้งาน Java แต่แทนที่จะเข้าใจวิธีการใช้ Java อย่างถูกต้อง ถ้ามันแยกไม่ออกในทางปฏิบัติแล้วก็จะไม่มีจุดสนใจและแม้แต่คิดว่ามันจะเสียเวลา
Loduwijk

331

Java ผ่านการอ้างอิงตามค่า

ดังนั้นคุณไม่สามารถเปลี่ยนการอ้างอิงที่ได้รับผ่าน


27
แต่สิ่งที่ยังคงเกิดขึ้นซ้ำ ๆ "คุณไม่สามารถเปลี่ยนค่าของวัตถุที่ส่งผ่านในการโต้แย้ง" เป็นเท็จอย่างชัดเจน คุณอาจไม่สามารถทำให้พวกเขาอ้างถึงวัตถุอื่น แต่คุณสามารถเปลี่ยนเนื้อหาของพวกเขาโดยการเรียกวิธีการของพวกเขา IMO หมายความว่าคุณสูญเสียผลประโยชน์ทั้งหมดของการอ้างอิงและไม่ได้รับการค้ำประกันเพิ่มเติม
Timmmm

34
ฉันไม่เคยพูดว่า "คุณไม่สามารถเปลี่ยนค่าของวัตถุที่ส่งผ่านในการโต้แย้ง" ฉันจะพูดว่า "คุณไม่สามารถเปลี่ยนค่าของการอ้างอิงวัตถุที่ส่งผ่านเป็นอาร์กิวเมนต์วิธี" ซึ่งเป็นคำสั่งที่แท้จริงเกี่ยวกับภาษา Java เห็นได้ชัดว่าคุณสามารถเปลี่ยนสถานะของวัตถุ (ตราบใดที่มันไม่เปลี่ยนรูป)
ScArcher2

20
โปรดทราบว่าคุณไม่สามารถส่งผ่านวัตถุใน java ได้จริง วัตถุยังคงอยู่บนกอง ตัวชี้ไปยังวัตถุสามารถส่งผ่าน (ซึ่งได้รับการคัดลอกลงในกรอบสแต็กสำหรับวิธีการที่เรียกว่า) ดังนั้นคุณจะไม่เปลี่ยนค่าการส่งผ่าน (ตัวชี้) แต่คุณมีอิสระที่จะติดตามและเปลี่ยนแปลงสิ่งต่างๆบนกองที่ชี้ไป นั่นคือการผ่านค่า
Scott Stanchfield

10
ดูเหมือนว่าคุณเพิ่งผ่านการอ้างอิงหรือไม่ ฉันจะปกป้องความจริงที่ว่า Java ยังคงเป็นภาษาที่ใช้อ้างอิงแบบคัดลอก ความจริงที่ว่ามันเป็นข้อมูลอ้างอิงที่คัดลอกไม่ได้เปลี่ยนคำศัพท์ ทั้งผู้อ้างอิงยังคงชี้ไปที่วัตถุเดียวกัน นี่เป็นข้อโต้แย้งของคนเจ้าระเบียบ ...
John Strickler

3
Java ไม่ผ่านวัตถุมันผ่านค่าของตัวชี้ไปยังวัตถุ สิ่งนี้จะสร้างตัวชี้ใหม่ไปยังตำแหน่งหน่วยความจำของวัตถุต้นฉบับในตัวแปรใหม่ หากคุณเปลี่ยนค่า (ที่อยู่หน่วยความจำชี้ไปที่) ของตัวแปรพอยน์เตอร์ในเมธอดดังนั้นพอยน์เตอร์ดั้งเดิมที่ใช้ในเมธอด caller จะไม่ถูกแก้ไข หากคุณกำลังเรียกพารามิเตอร์อ้างอิงจากนั้นความจริงที่ว่ามันเป็นสำเนาของการอ้างอิงดั้งเดิมไม่ใช่การอ้างอิงดั้งเดิมนั้นเองซึ่งขณะนี้มีการอ้างอิงสองครั้งไปยังวัตถุหมายความว่ามันเป็นแบบ pass-by-value
theferrit32

238

ฉันรู้สึกว่าการโต้เถียงเกี่ยวกับ "การอ้างอิงแบบอ้างอิงเทียบกับการส่งต่อค่า" ไม่เป็นประโยชน์มาก

หากคุณพูดว่า "Java เป็นแบบบายพาส (อ้างอิง / ค่า)" ไม่ว่าในกรณีใดคุณจะไม่ได้รับคำตอบที่สมบูรณ์ นี่คือข้อมูลเพิ่มเติมที่หวังว่าจะช่วยในการทำความเข้าใจสิ่งที่เกิดขึ้นในหน่วยความจำ

หลักสูตร Crash บน stack / heap ก่อนที่เราจะนำ Java ไปใช้: ค่าจะเข้าและออกจาก stack ในรูปแบบที่เป็นระเบียบเรียบร้อยดีเช่นสแต็กของเพลทที่โรงอาหาร หน่วยความจำในฮีป (เรียกอีกอย่างว่าหน่วยความจำแบบไดนามิก) เป็นแบบจับจดและไม่เป็นระเบียบ JVM จะค้นหาพื้นที่ว่างในที่ที่สามารถทำได้และทำให้เป็นอิสระเนื่องจากตัวแปรที่ใช้ไม่จำเป็นต้องใช้อีกต่อไป

ตกลง. ก่อนอื่นดั้งเดิมในพื้นที่จะไปที่สแต็ก ดังนั้นรหัสนี้:

int x = 3;
float y = 101.1f;
boolean amIAwesome = true;

ผลลัพธ์ในสิ่งนี้:

primitives บนสแต็ก

เมื่อคุณประกาศและยกตัวอย่างวัตถุ วัตถุที่เกิดขึ้นจริงไปที่กอง เกิดอะไรขึ้นกับสแต็ก ที่อยู่ของวัตถุบนกอง โปรแกรมเมอร์ C ++ จะเรียกสิ่งนี้ว่าตัวชี้ แต่นักพัฒนา Java บางคนนั้นขัดแย้งกับคำว่า "ตัวชี้" อะไรก็ตาม เพิ่งรู้ว่าที่อยู่ของวัตถุนั้นอยู่ในกองซ้อน

ชอบมาก

int problems = 99;
String name = "Jay-Z";

ab * 7ch ไม่ใช่หนึ่ง!

อาเรย์เป็นวัตถุดังนั้นมันจึงไปที่ฮีปเช่นกัน แล้ววัตถุในอาเรย์ล่ะ? พวกเขาได้รับพื้นที่ฮีพของตัวเองและที่อยู่ของวัตถุแต่ละชิ้นจะอยู่ในอาร์เรย์

JButton[] marxBros = new JButton[3];
marxBros[0] = new JButton("Groucho");
marxBros[1] = new JButton("Zeppo");
marxBros[2] = new JButton("Harpo");

พี่น้องมาร์กซ์

ดังนั้นสิ่งที่จะถูกส่งผ่านเมื่อคุณเรียกใช้เมธอด? หากคุณผ่านวัตถุสิ่งที่คุณกำลังส่งผ่านจริงๆคือที่อยู่ของวัตถุ บางคนอาจพูดว่า "คุณค่า" ของที่อยู่และบางคนบอกว่ามันเป็นเพียงการอ้างอิงถึงวัตถุ นี่คือต้นกำเนิดของสงครามศักดิ์สิทธิ์ระหว่างผู้เสนอ "อ้างอิง" และ "คุณค่า" สิ่งที่คุณเรียกว่าไม่สำคัญเท่ากับที่คุณเข้าใจว่าสิ่งที่ได้รับการส่งผ่านเป็นที่อยู่ไปยังวัตถุ

private static void shout(String name){
    System.out.println("There goes " + name + "!");
}

public static void main(String[] args){
    String hisName = "John J. Jingleheimerschmitz";
    String myName = hisName;
    shout(myName);
}

หนึ่งสตริงได้รับการสร้างและพื้นที่สำหรับมันถูกจัดสรรในฮีปและที่อยู่ในสตริงจะถูกเก็บไว้ในสแต็คและได้รับตัวระบุhisNameเนื่องจากที่อยู่ของสตริงที่สองเหมือนกันเป็นครั้งแรกไม่มีการสร้างสตริงใหม่และ ไม่มีการจัดสรรพื้นที่ฮีปใหม่ แต่จะมีการสร้างตัวระบุใหม่บนสแต็ก จากนั้นเราเรียกshout(): สร้างเฟรมสแต็กใหม่และตัวระบุใหม่nameถูกสร้างและกำหนดที่อยู่ของสตริงที่มีอยู่แล้ว

la da di da da da

ดังนั้นค่าอ้างอิง? คุณพูดว่า "มันฝรั่ง"


7
อย่างไรก็ตามคุณควรติดตามตัวอย่างที่ซับซ้อนมากขึ้นซึ่งฟังก์ชั่นจะเปลี่ยนตัวแปรให้เป็นที่อยู่ซึ่งมีการอ้างอิง
Brian Peterson

34
ผู้คนไม่ได้ "เต้นรำกับปัญหาที่แท้จริง" ของ stack vs heap เพราะนั่นไม่ใช่ปัญหาจริง มันเป็นรายละเอียดการปฏิบัติที่ดีที่สุดและผิดอย่างร้ายแรงที่สุด (มันเป็นไปได้มากทีเดียวสำหรับวัตถุที่จะมีชีวิตอยู่ในกอง; google "การวิเคราะห์การหลบหนี" และจำนวนมากของวัตถุที่มีพื้นฐานที่น่าจะ. ไม่ได้อยู่บนสแต็ค.) ปัญหาที่แท้จริงคือว่าความแตกต่างระหว่างประเภทของการอ้างอิงและประเภทค่า - โดยเฉพาะอย่างยิ่งว่าค่าของตัวแปรชนิดการอ้างอิงเป็นการอ้างอิงไม่ใช่วัตถุที่อ้างถึง
cHao

8
มันเป็น "รายละเอียดการดำเนินงาน" ในการที่ Java จะไม่จำเป็นต้องแสดงให้คุณเห็นจริงในกรณีที่ชีวิตของวัตถุที่อยู่ในหน่วยความจำและในความเป็นจริงดูเหมือนว่ามุ่งมั่นที่จะหลีกเลี่ยงการรั่วไหลของข้อมูลที่ มันสามารถวางวัตถุลงบนสแต็กและคุณไม่มีทางรู้ หากคุณสนใจคุณกำลังมุ่งเน้นไปที่สิ่งที่ผิด - และในกรณีนี้นั่นหมายถึงการเพิกเฉยต่อปัญหาที่แท้จริง
cHao

10
และอย่างใดอย่างหนึ่ง "ดั้งเดิมไปในกอง" ไม่ถูกต้อง ตัวแปรโลคัลดั้งเดิมจะอยู่บนสแต็ก (หากยังไม่ได้รับการปรับให้เหมาะสม) แต่ถ้าอย่างนั้นตัวแปรอ้างอิงในท้องถิ่นก็ควรทำเช่นนั้น และสมาชิกดึกดำบรรพ์ที่นิยามไว้ภายในวัตถุจะมีชีวิตอยู่ทุกที่ที่วัตถุนั้นมีชีวิตอยู่
cHao

9
เห็นด้วยกับความคิดเห็นที่นี่ Stack / heap เป็นปัญหาด้านข้างและไม่เกี่ยวข้อง ตัวแปรบางตัวอาจอยู่ในสแต็กบางตัวอยู่ในหน่วยความจำแบบคงที่ (ตัวแปรแบบคงที่) และมีอยู่มากมายบนฮีป (ตัวแปรสมาชิกวัตถุทั้งหมด) ไม่มีตัวแปรเหล่านี้สามารถส่งผ่านโดยการอ้างอิง: จากวิธีการที่เรียกว่ามันเป็นไปไม่ได้ที่จะเปลี่ยนค่าของตัวแปรที่ถูกส่งผ่านเป็นอาร์กิวเมนต์ ดังนั้นจึงไม่มีการอ้างอิงผ่านใน Java
fishinear

195

เพียงเพื่อแสดงความคมชัดเปรียบเทียบข้อมูลโค้ด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"
}

ในชวา

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 มีการส่งผ่านสองประเภทเท่านั้น: ตามค่าสำหรับชนิดในตัวและตามค่าของตัวชี้สำหรับประเภทวัตถุ


7
+1 ฉันจะเพิ่มDog **objPtrPtrในตัวอย่าง C ++ ด้วยวิธีนี้เราสามารถแก้ไขสิ่งที่ตัวชี้ "ชี้ไปที่"
Amro

1
แต่นี่ไม่ใช่คำตอบสำหรับคำถามที่ให้ใน Java ตามความเป็นจริงทุกอย่างในวิธีของ Java คือ Pass By Value ไม่มีอะไรเพิ่มเติม
tauitdnmd

2
ตัวอย่างนี้พิสูจน์ให้เห็นว่า java ใช้ตัวชี้ที่เทียบเท่ากันมากใน C no? ที่ผ่านค่าใน C เป็นพื้นอ่านเท่านั้น? ในตอนท้ายหัวข้อทั้งหมดนี้ไม่สามารถเข้าใจได้
amdev

179

Java ผ่านการอ้างอิงไปยังวัตถุตามค่า


นี่คือคำตอบที่ดีที่สุด สั้นและถูกต้อง
woens

166

โดยทั่วไปการกำหนดพารามิเตอร์ 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


8
ฉันอยากจะบอกว่าแถบนั้นเป็นสำเนาของ baz อ้างอิง (หรือนามแฝง baz) ซึ่งชี้ไปที่วัตถุต้นเดียวกัน
MaxZoom

ไม่แตกต่างกันเล็กน้อยระหว่างคลาส String และคลาสอื่นทั้งหมดหรือไม่
Mehdi Karamosly

152

ฉันไม่อยากจะเชื่อเลยว่าไม่มีใครพูดถึง Barbara Liskov เลย เมื่อเธอออกแบบ CLU ในปี 1974 เธอพบปัญหาคำศัพท์เดียวกันนี้และเธอคิดค้นคำเรียกโดยการแบ่งปัน (หรือเรียกอีกอย่างว่าการโทรโดยการแชร์วัตถุและการโทรโดยวัตถุ ) สำหรับกรณีเฉพาะของ "การโทรตามค่าโดยที่ค่าคือ ข้อมูลอ้างอิง "


3
ฉันชอบความแตกต่างนี้ในระบบการตั้งชื่อ โชคไม่ดีที่ Java รองรับการโทรด้วยการแชร์สำหรับวัตถุ แต่ไม่ใช่การโทรตามค่า (เช่นเดียวกับ C ++) Java รองรับการโทรตามค่าสำหรับประเภทข้อมูลดั้งเดิมเท่านั้นและไม่ใช่ประเภทข้อมูลประกอบ
Derek Mahar

2
ฉันไม่คิดว่าเราต้องการคำศัพท์พิเศษ - เป็นเพียงแค่การส่งต่อค่าสำหรับประเภทเฉพาะ จะเพิ่ม "การโทรโดยดั้งเดิม" เพิ่มการชี้แจงใด ๆ
Scott Stanchfield


ดังนั้นฉันสามารถส่งต่อแบบอ้างอิงโดยแชร์วัตถุบริบทส่วนกลางหรือแม้แต่ผ่านวัตถุบริบทที่มีการอ้างอิงอื่น ฉันยังคงใช้การส่งต่อตามตัวอักษร แต่อย่างน้อยฉันก็สามารถเข้าถึงการอ้างอิงที่ฉันสามารถแก้ไขและทำให้พวกเขาชี้ไปที่สิ่งอื่น
YoYo

1
@Sanjeev: การโทรหาโดยการใช้วัตถุร่วมกันเป็นกรณีพิเศษของการส่งต่อค่า อย่างไรก็ตามผู้คนจำนวนมากแย้งว่า Java (และภาษาที่คล้ายคลึงกันเช่น Python, Ruby, ECMAScript, Smalltalk) เป็นแบบอ้างถึง ฉันชอบที่จะเรียกมันผ่านโดยค่าเกินไป แต่โทรโดยวัตถุที่ใช้ร่วมกันน่าจะเป็นระยะที่เหมาะสมที่แม้กระทั่งคนที่อ้างว่ามันไม่ผ่านโดยค่าสามารถตกลงที่จะ
Jörg W Mittag

119

ประเด็นสำคัญของเรื่องคือการอ้างอิงคำในการแสดงออก "ผ่านการอ้างอิง" หมายถึงสิ่งที่แตกต่างอย่างสิ้นเชิงจากความหมายปกติของการอ้างอิงคำใน Java

มักจะอยู่ใน Java อ้างอิงหมายความ AA อ้างอิงถึงวัตถุ แต่ข้อกำหนดทางเทคนิคส่งผ่านโดยการอ้างอิง / ค่าจากทฤษฎีภาษาโปรแกรมกำลังพูดถึงการอ้างอิงถึงเซลล์หน่วยความจำถือตัวแปรซึ่งเป็นสิ่งที่แตกต่างอย่างสิ้นเชิง


7
@Gevorg - แล้ว "NullPointerException" คืออะไร?
Hot Licks

5
@Hot: เป็นข้อยกเว้นที่น่าเสียดายที่มีชื่อจากก่อนที่ Java จะตัดสินด้วยคำศัพท์ที่ชัดเจน ข้อยกเว้นเชิงความหมายเทียบเท่าใน c # เรียกว่า NullReferenceException
JacquesB

4
สำหรับฉันแล้วดูเหมือนว่าการใช้ "อ้างอิง" ในคำศัพท์ภาษาจาวาเป็นสิ่งที่ส่งผลกระทบต่อความเข้าใจ
Hot Licks

ฉันเรียนรู้ที่จะเรียกสิ่งเหล่านี้เรียกว่า "พอยน์เตอร์ไปยังวัตถุ" เป็น "การจัดการกับวัตถุ" สิ่งนี้ลดความคลุมเครือ
moronkreacionz

86

ใน java ทุกอย่างเป็นการอ้างอิงดังนั้นเมื่อคุณมีสิ่งที่ชอบ: Point pnt1 = new Point(0,0);Java ทำตาม:

  1. สร้างวัตถุ Point ใหม่
  2. สร้างการอ้างอิงจุดใหม่และเริ่มต้นการอ้างอิงนั้นไปยังจุด (อ้างอิงถึง)กับวัตถุจุดที่สร้างไว้ก่อนหน้านี้
  3. จากจุดนี้ไปจนถึงอายุการใช้งานของวัตถุคุณจะสามารถเข้าถึงวัตถุนั้นได้ผ่านการอ้างอิง pnt1 ดังนั้นเราจึงสามารถพูดได้ว่าใน Java คุณจัดการวัตถุผ่านการอ้างอิง

ป้อนคำอธิบายรูปภาพที่นี่

Java ไม่ผ่านการขัดแย้งวิธีโดยอ้างอิง; มันผ่านพวกเขาตามค่า ฉันจะใช้ตัวอย่างจากเว็บไซต์นี้ :

public static void tricky(Point arg1, Point arg2) {
  arg1.x = 100;
  arg1.y = 100;
  Point temp = arg1;
  arg1 = arg2;
  arg2 = temp;
}
public static void main(String [] args) {
  Point pnt1 = new Point(0,0);
  Point pnt2 = new Point(0,0);
  System.out.println("X1: " + pnt1.x + " Y1: " +pnt1.y); 
  System.out.println("X2: " + pnt2.x + " Y2: " +pnt2.y);
  System.out.println(" ");
  tricky(pnt1,pnt2);
  System.out.println("X1: " + pnt1.x + " Y1:" + pnt1.y); 
  System.out.println("X2: " + pnt2.x + " Y2: " +pnt2.y);  
}

การไหลของโปรแกรม:

Point pnt1 = new Point(0,0);
Point pnt2 = new Point(0,0);

การสร้างวัตถุ Point สองแบบที่แตกต่างกันโดยมีการอ้างอิงที่แตกต่างกันสองรายการ ป้อนคำอธิบายรูปภาพที่นี่

System.out.println("X1: " + pnt1.x + " Y1: " +pnt1.y); 
System.out.println("X2: " + pnt2.x + " Y2: " +pnt2.y);
System.out.println(" ");

ตามที่คาดหวังเอาท์พุทจะเป็น:

X1: 0     Y1: 0
X2: 0     Y2: 0

ในบรรทัดนี้ 'pass-by-value' เข้าสู่การเล่น ...

tricky(pnt1,pnt2);           public void tricky(Point arg1, Point arg2);

การอ้างอิงpnt1และpnt2ถูกส่งผ่านโดยมูลค่าไปยังวิธีการที่ยุ่งยากซึ่งหมายความว่าตอนนี้การอ้างอิงของคุณpnt1และpnt2มีcopiesชื่อarg1และarg2. So pnt1และarg1 ชี้ไปที่วัตถุเดียวกัน (เหมือนกันสำหรับpnt2และarg2) ป้อนคำอธิบายรูปภาพที่นี่

ในtrickyวิธีการ:

 arg1.x = 100;
 arg1.y = 100;

ป้อนคำอธิบายรูปภาพที่นี่

ถัดไปในtrickyวิธีการ

Point temp = arg1;
arg1 = arg2;
arg2 = temp;

ที่นี่คุณสร้างtempการอ้างอิงจุดใหม่ก่อนซึ่งจะชี้ไปที่สถานที่เดียวกันเช่นarg1การอ้างอิง จากนั้นคุณย้ายการอ้างอิงarg1เพื่อชี้ไปที่ตำแหน่งเดียวกันเช่นarg2การอ้างอิง ในที่สุดก็arg2จะชี้tempไปยังสถานที่เดียวกันอย่างเช่น

ป้อนคำอธิบายรูปภาพที่นี่

จากที่นี่ขอบเขตของวิธีการที่จะหายไปและคุณไม่ได้มีการเข้าถึงมากขึ้นในการอ้างอิง:tricky , , แต่หมายเหตุสำคัญคือทุกสิ่งที่คุณทำกับการอ้างอิงเหล่านี้เมื่อพวกเขา 'ในชีวิต' จะส่งผลกระทบต่อวัตถุที่พวกเขาจะชี้ไปarg1arg2temp

ดังนั้นหลังจากดำเนินการวิธีการtrickyเมื่อคุณกลับไปที่mainคุณมีสถานการณ์นี้: ป้อนคำอธิบายรูปภาพที่นี่

ดังนั้นตอนนี้การดำเนินการของโปรแกรมอย่างสมบูรณ์จะเป็น:

X1: 0         Y1: 0
X2: 0         Y2: 0
X1: 100       Y1: 100
X2: 0         Y2: 0

7
ในจาวาเมื่อคุณพูดว่า "ใน java ทุกอย่างเป็นการอ้างอิง" คุณหมายถึงวัตถุทั้งหมดจะถูกส่งผ่านโดยการอ้างอิง ชนิดข้อมูลดั้งเดิมไม่ถูกส่งผ่านโดยการอ้างอิง
Eric

ฉันสามารถพิมพ์ค่าที่สลับในวิธีหลักได้ ในวิธีการ trickey เพิ่มคำสั่งต่อไปนี้arg1.x = 1; arg1.y = 1; arg2.x = 2; arg2.y = 2;ดังนั้นเป็น arg1 ตอนนี้ถือp ref2 refrence และ arg2 ถือตอนนี้อ้างอิง pnt1 ดังนั้นการพิมพ์X1: 2 Y1: 2 X2: 1 Y2: 1
Shahid Ghafoor

85

Java ผ่านค่าเสมอไม่ใช่ผ่านการอ้างอิง

ก่อนอื่นเราต้องเข้าใจว่าอะไรคือคุณค่าและคุณค่าของการอ้างอิง

ผ่านหมายถึงค่าที่คุณกำลังทำสำเนาในหน่วยความจำของค่าพารามิเตอร์ที่เกิดขึ้นจริงที่ถูกส่งผ่านไปใน. นี้เป็นสำเนาของเนื้อหาของพารามิเตอร์ที่เกิดขึ้นจริง

ผ่านโดยการอ้างอิง (เรียกว่าผ่านที่อยู่) หมายถึงว่าสำเนาที่อยู่ของพารามิเตอร์ที่เกิดขึ้นจริงจะถูกเก็บไว้

บางครั้ง Java สามารถให้ภาพลวงตาของการส่งผ่านโดยอ้างอิง มาดูกันว่ามันทำงานอย่างไรโดยใช้ตัวอย่างด้านล่าง:

public class PassByValue {
    public static void main(String[] args) {
        Test t = new Test();
        t.name = "initialvalue";
        new PassByValue().changeValue(t);
        System.out.println(t.name);
    }

    public void changeValue(Test f) {
        f.name = "changevalue";
    }
}

class Test {
    String name;
}

ผลลัพธ์ของโปรแกรมนี้คือ:

changevalue

มาทำความเข้าใจทีละขั้นตอน:

Test t = new Test();

ในขณะที่เราทุกคนรู้ว่ามันจะสร้างวัตถุในกองและคืนค่าอ้างอิงกลับไปที่ t ตัวอย่างเช่นสมมติว่าค่าของ t คือ0x100234(เราไม่ทราบค่าภายใน JVM จริงนี่เป็นเพียงตัวอย่าง)

ภาพประกอบแรก

new PassByValue().changeValue(t);

เมื่อผ่านการอ้างอิง t ไปยังฟังก์ชั่นมันจะไม่ผ่านค่าอ้างอิงจริงของการทดสอบวัตถุโดยตรง แต่มันจะสร้างสำเนาของ t แล้วส่งผ่านไปยังฟังก์ชัน เนื่องจากมันผ่านค่ามันจะผ่านสำเนาของตัวแปรมากกว่าการอ้างอิงจริงของมัน เนื่องจากเราบอกว่าค่าของ t คือ0x100234ทั้ง t และ f จะมีค่าเท่ากันและด้วยเหตุนี้พวกเขาจะชี้ไปที่วัตถุเดียวกัน

ภาพประกอบที่สอง

หากคุณเปลี่ยนแปลงสิ่งใดก็ตามในฟังก์ชันโดยใช้การอ้างอิง f มันจะแก้ไขเนื้อหาที่มีอยู่ของวัตถุ นั่นคือเหตุผลที่เราได้รับผลลัพธ์changevalueซึ่งมีการอัพเดทในฟังก์ชั่น

เพื่อให้เข้าใจชัดเจนยิ่งขึ้นลองพิจารณาตัวอย่างต่อไปนี้:

public class PassByValue {
    public static void main(String[] args) {
        Test t = new Test();
        t.name = "initialvalue";
        new PassByValue().changeRefence(t);
        System.out.println(t.name);
    }

    public void changeRefence(Test f) {
        f = null;
    }
}

class Test {
    String name;
}

จะโยนนี้NullPointerExceptionหรือไม่? ไม่เพราะจะส่งสำเนาของการอ้างอิงเท่านั้น ในกรณีของการอ้างอิงโดยอ้างอิงมันอาจจะโยน a NullPointerExceptionดังที่เห็นด้านล่าง:

ภาพประกอบที่สาม

หวังว่านี่จะช่วยได้


71

การอ้างอิงจะเป็นค่าเมื่อแสดงเสมอไม่ว่าคุณจะใช้ภาษาใด

รับมุมมองด้านนอกให้ดูที่ Assembly หรือการจัดการหน่วยความจำระดับต่ำ ที่ระดับ CPU การอ้างอิงไปยังสิ่งใด ๆ จะกลายเป็นค่าทันทีหากได้รับการเขียนไปยังหน่วยความจำหรือลงทะเบียน CPU ตัวใดตัวหนึ่ง (นั่นคือสาเหตุที่ตัวชี้เป็นคำจำกัดความที่ดีมันเป็นค่าที่มีจุดประสงค์ในเวลาเดียวกัน)

ข้อมูลในหน่วยความจำมีตำแหน่งที่ตั้งและที่ตำแหน่งนั้นมีค่า (ไบต์คำอะไรก็ตาม) ในแอสเซมบลีเรามีวิธีแก้ปัญหาที่สะดวกในการตั้งชื่อให้กับบางตำแหน่ง (ตัวแปร aka) แต่เมื่อรวบรวมรหัสแอสเซมเบลอร์จะแทนที่ชื่อด้วยตำแหน่งที่กำหนดเหมือนกับเบราว์เซอร์ของคุณจะแทนที่ชื่อโดเมนด้วยที่อยู่ IP

ในทางเทคนิคแล้วมันเป็นไปไม่ได้ที่จะส่งต่อการอ้างอิงไปยังสิ่งใด ๆ ในภาษาใด ๆ โดยไม่ต้องแสดงถึงมัน (เมื่อมันกลายเป็นค่าทันที)

ให้บอกว่าเรามีตัวแปร Foo ตำแหน่งของมันอยู่ที่ 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 register (เช่น. EAX)
  2. 5 รับ PUSHd ไปยังสแต็ก
  3. 47 ได้รับการคัดลอกไปยังหนึ่งใน CPU ที่ลงทะเบียน
  4. 47 PUSHd ไปยังสแต็ก
  5. 223 ได้รับการคัดลอกไปยังหนึ่งใน CPU ที่ลงทะเบียน
  6. 223 รับ PUSHd ไปยังสแต็ก

ในทุกกรณีข้างต้นค่า - สำเนาของค่าที่มีอยู่ - ถูกสร้างขึ้นตอนนี้มันเกินวิธีการรับเพื่อจัดการมัน เมื่อคุณเขียน "ฟู" ในวิธีการที่มันเป็นอย่างใดอย่างหนึ่งอ่านออกจาก EAX หรือโดยอัตโนมัติ dereferencedหรือดับเบิล dereferenced กระบวนการขึ้นอยู่กับวิธีการทำงานของภาษาและ / หรือสิ่งที่ประเภทของคำสั่งฟู สิ่งนี้ถูกซ่อนไว้จากนักพัฒนาซอฟต์แวร์จนกว่าเธอจะหลีกเลี่ยงกระบวนการยกเลิกการพิจารณาคดี ดังนั้นการอ้างอิงจึงเป็นค่าเมื่อแสดงเพราะการอ้างอิงเป็นค่าที่ต้องดำเนินการ (ที่ระดับภาษา)

ตอนนี้เราได้ผ่าน Foo ไปยังวิธีการ:

  • ในกรณีที่ 1 และ 2 หากคุณเปลี่ยน Foo ( Foo = 9) จะมีผลกับขอบเขตในเครื่องเท่านั้นเนื่องจากคุณมีสำเนาของค่า จากภายในวิธีที่เราไม่สามารถกำหนดได้ว่าอยู่ที่ไหนในหน่วยความจำเดิมของ Foo
  • ในกรณีที่ 3 และ 4 หากคุณใช้การสร้างภาษาเริ่มต้นและเปลี่ยน Foo ( Foo = 11) ก็สามารถเปลี่ยน Foo ทั่วโลก (ขึ้นอยู่กับภาษาเช่น Java หรือชอบ Pascal ของprocedure findMin(x, y, z: integer;var m: integer); ) อย่างไรก็ตามหากภาษานั้นอนุญาตให้คุณหลีกเลี่ยงกระบวนการที่47ต้องพูดถึงคุณสามารถเปลี่ยนพูด49ได้ ณ จุดนั้นดูเหมือนว่าจะมีการเปลี่ยนแปลงถ้าคุณอ่านเพราะคุณได้เปลี่ยนตัวชี้ท้องถิ่นเป็น และถ้าคุณต้องแก้ไข Foo นี้ภายในเมธอด ( Foo = 12) คุณอาจจะ FUBAR เรียกใช้งานโปรแกรม (aka. segfault) เพราะคุณจะเขียนไปยังหน่วยความจำที่แตกต่างไปจากที่คาดไว้คุณสามารถปรับเปลี่ยนพื้นที่ที่ถูกกำหนดให้ใช้งานได้ โปรแกรมและการเขียนถึงมันจะแก้ไขรหัสที่ใช้ (Foo ไม่ได้อยู่ที่47) แต่มูลค่าของ Foo ของ47ไม่ได้เปลี่ยนไปทั่วโลกเพียงอันเดียวในวิธีการเพราะ47เป็นสำเนาไปยังวิธีการ
  • ในกรณีที่ 5 และ 6 ถ้าคุณปรับเปลี่ยน223ภายในวิธีการมันจะสร้างความโกลาหลเช่นเดียวกับใน 3 หรือ 4 (ตัวชี้ชี้ไปที่ค่าที่ไม่ดีตอนนี้ที่ใช้อีกครั้งเป็นตัวชี้) แต่นี่ยังคงเป็นท้องถิ่น ปัญหาเป็น 223 ถูกคัดลอก อย่างไรก็ตามหากคุณสามารถตรวจสอบได้Ref2Foo(กล่าวคือ223) เข้าถึงและแก้ไขค่าที่ชี้47ไปยัง49มันจะมีผลกับ Foo ทั่วโลกเพราะในกรณีนี้วิธีการได้รับสำเนาของ223 แต่การอ้างอิง47มีอยู่เพียงครั้งเดียวและเปลี่ยน เพื่อ49จะนำไปสู่Ref2Fooการอ้างอิงซ้ำทุกครั้งเป็นค่าที่ผิด

การใส่รายละเอียดที่ไม่สำคัญแม้แต่ภาษาที่ใช้การอ้างอิงแบบอ้างอิงถึงกันจะส่งผ่านค่าไปยังฟังก์ชั่น แต่ฟังก์ชั่นเหล่านั้นรู้ว่าพวกเขาจะต้องใช้มันเพื่อวัตถุประสงค์ในการยกเลิกการอ้างอิง นี้ผ่านที่อ้างอิงเป็นค่าที่ถูกซ่อนอยู่เพียงโปรแกรมเมอร์เพราะมันจะไม่ได้ผลในทางปฏิบัติและคำศัพท์เป็นเพียงการส่งผ่านโดยการอ้างอิง

การส่งผ่านค่าที่เข้มงวดก็ไม่มีประโยชน์เช่นกันหมายความว่าอาร์เรย์ 100 Mbyte ควรถูกคัดลอกทุกครั้งที่เราเรียกใช้เมธอดที่มีอาเรย์เป็นอาร์กิวเมนต์ดังนั้น Java จึงไม่สามารถผ่านค่าที่เข้มงวดได้ ทุกภาษาจะผ่านการอ้างอิงไปยังอาเรย์ขนาดใหญ่นี้ (เป็นค่า) และใช้กลไกการคัดลอกเมื่อเขียนหากอาเรย์นั้นสามารถเปลี่ยนในเครื่องภายในวิธีการหรืออนุญาตให้เมธอด (อย่างที่ Java ทำ) เปลี่ยนอาเรย์ทั่วโลก มุมมองของผู้โทร) และภาษาสองสามภาษาอนุญาตให้แก้ไขค่าของการอ้างอิงเอง

ดังนั้นในระยะสั้นและในคำศัพท์ของตัวเองของ Java, Java คือผ่านโดยมีมูลค่าที่คุ้มค่าสามารถ: ทั้งมูลค่าที่แท้จริงหรือค่าที่เป็นตัวแทนของการอ้างอิง


1
ในภาษาที่มีการอ้างอิงแบบอ้างอิงผ่านสิ่งที่ส่งผ่าน (ข้อมูลอ้างอิง) นั้นเป็นข้อมูลชั่วคราว ผู้รับไม่ได้ "ควร" คัดลอก ใน Java การส่งผ่านอาร์เรย์จะถูกส่งเป็น "ตัวระบุวัตถุ" - เทียบเท่ากับใบบันทึกซึ่งระบุว่า "Object # 24601" เมื่อวัตถุ 24601 ที่สร้างขึ้นนั้นเป็นอาร์เรย์ ผู้รับสามารถคัดลอก "Object # 24601" ได้ทุกที่ที่ต้องการและทุกคนที่มีกระดาษเขียนว่า "Object # 24601" สามารถทำสิ่งใดก็ได้ตามต้องการด้วยองค์ประกอบอาเรย์ใด ๆ รูปแบบของบิตที่ผ่านไปจะไม่ได้พูดว่า "Object # 24601" แน่นอน แต่ ...
supercat

... ประเด็นสำคัญก็คือผู้รับของ object-id ที่ระบุ array สามารถจัดเก็บ object-id ได้ทุกที่ที่ต้องการและมอบให้ใครก็ตามที่มันต้องการและผู้รับก็จะสามารถเข้าถึงหรือแก้ไขอาร์เรย์ได้ทุกเมื่อ ต้องการ ในทางตรงกันข้ามถ้าอาเรย์ถูกส่งผ่านโดยการอ้างอิงในภาษาอย่าง Pascal ซึ่งสนับสนุนสิ่งต่าง ๆ เมธอดที่เรียกว่าสามารถทำสิ่งที่มันต้องการกับอาเรย์ได้ แต่ไม่สามารถเก็บการอ้างอิงในลักษณะที่จะอนุญาตให้โค้ดแก้ไขอาเรย์ หลังจากมันกลับมา
supercat

แน่นอนใน Pascal คุณจะได้รับที่อยู่ของตัวแปรทุกตัวไม่ว่าจะเป็นการส่งต่อโดยอ้างอิงหรือคัดลอกในเครื่องaddrไม่พิมพ์, @ทำกับชนิดและคุณสามารถแก้ไขตัวแปรอ้างอิงได้ในภายหลัง (ยกเว้นที่คัดลอกไว้ภายในเครื่อง) แต่ฉันไม่เห็นว่าทำไมคุณถึงทำเช่นนั้น กระดาษแผ่นนั้นในตัวอย่างของคุณ (Object # 24601) เป็นการอ้างอิงโดยมีวัตถุประสงค์เพื่อช่วยค้นหาอาร์เรย์ในหน่วยความจำ แต่ไม่มีข้อมูลอาร์เรย์ในตัวเอง หากคุณรีสตาร์ทโปรแกรมอาร์เรย์เดียวกันอาจได้รับ object-id ที่แตกต่างกันแม้ว่าเนื้อหาของมันจะเหมือนกันกับที่เคยเป็นในการรันครั้งก่อน
karatedog

ฉันคิดว่าตัวดำเนินการ "@" ไม่ใช่ส่วนหนึ่งของ Pascal มาตรฐาน แต่ถูกนำไปใช้เป็นส่วนขยายทั่วไป มันเป็นส่วนหนึ่งของมาตรฐานหรือไม่? จุดของฉันคือว่าในภาษาที่มีการส่งผ่านโดยแท้จริงและไม่มีความสามารถในการสร้างตัวชี้ที่ไม่ใช่ชั่วคราวไปยังวัตถุชั่วคราวรหัสที่เก็บการอ้างอิงเท่านั้นไปยังอาร์เรย์ที่ใดก็ได้ในจักรวาลก่อนผ่านอาร์เรย์ การอ้างอิงสามารถรู้ได้ว่าหากผู้รับ "กลโกง" มันจะยังคงมีการอ้างอิงเท่านั้นหลังจากนั้น วิธีเดียวที่ปลอดภัยที่จะทำให้สำเร็จใน Java คือการสร้างวัตถุชั่วคราว ...
supercat

... ซึ่งสรุปAtomicReferenceและไม่เปิดเผยการอ้างอิงหรือเป้าหมาย แต่รวมถึงวิธีการที่จะทำสิ่งต่าง ๆ กับเป้าหมาย; เมื่อรหัสที่วัตถุถูกส่งกลับมาคืนค่าAtomicReference[ที่ผู้สร้างเก็บการอ้างอิงโดยตรง] ควรถูกยกเลิกและถูกยกเลิก นั่นจะให้ความหมายที่ถูกต้อง แต่มันจะช้าและไม่ดี
supercat

70

Java เป็นการโทรตามค่า

มันทำงานอย่างไร

  • คุณส่งสำเนาบิตของค่าอ้างอิงเสมอ!

  • ถ้ามันเป็นชนิดข้อมูลดั้งเดิมบิตเหล่านี้มีค่าของชนิดข้อมูลดั้งเดิมนั้นเองนั่นคือสาเหตุที่ถ้าเราเปลี่ยนค่าของส่วนหัวภายในวิธีการนั้นจะไม่สะท้อนการเปลี่ยนแปลงภายนอก

  • หากเป็นประเภทข้อมูลวัตถุเช่นFoo foo = new Foo ()ดังนั้นในกรณีนี้สำเนาที่อยู่ของวัตถุที่ผ่านไปเช่นทางลัดไฟล์สมมติว่าเรามีไฟล์ข้อความabc.txtที่C: \ desktopและสมมติว่าเราสร้างทางลัดของ ไฟล์เดียวกันและวางสิ่งนี้ไว้ในC: \ desktop \ abc-ทางลัดดังนั้นเมื่อคุณเข้าถึงไฟล์จากC: \ desktop \ abc.txtและเขียน'Stack Overflow'แล้วปิดไฟล์และเปิดไฟล์จากทางลัดอีกครั้ง เขียน'เป็นชุมชนออนไลน์ที่ใหญ่ที่สุดสำหรับโปรแกรมเมอร์ที่จะเรียนรู้'จากนั้นการเปลี่ยนแปลงไฟล์ทั้งหมดจะเป็น'Stack Overflow เป็นชุมชนออนไลน์ที่ใหญ่ที่สุดสำหรับโปรแกรมเมอร์ที่จะเรียนรู้'ซึ่งหมายความว่าไม่สำคัญว่าคุณจะเปิดไฟล์จากที่ใดในแต่ละครั้งที่เราเข้าถึงไฟล์เดียวกันที่นี่เราสามารถถือว่าFooเป็นไฟล์และสมมติว่า foo เก็บไว้ที่123hd7h (ที่อยู่ดั้งเดิมเช่นC: \ desktop \ abc.txt ) ที่อยู่และ234jdid (ที่อยู่ที่คัดลอกเช่นC: \ desktop \ abc- ทางลัดซึ่งอันที่จริงมีที่อยู่เดิมของไฟล์ที่อยู่ภายใน) .. ดังนั้นเพื่อความเข้าใจที่ดีขึ้นทำให้ไฟล์ทางลัดและความรู้สึก ..


66

มีคำตอบที่ดีอยู่แล้วซึ่งครอบคลุมในเรื่องนี้ ฉันต้องการบริจาคเล็ก ๆ ด้วยการแบ่งปันตัวอย่างง่ายๆ (ซึ่งจะรวบรวม) เปรียบเทียบพฤติกรรมระหว่าง Pass-by-Reference ใน c ++ และ Pass-by-value ใน Java

คะแนนน้อย:

  1. คำว่า "การอ้างอิง" นั้นมีความหมายมากเกินไปโดยมีความหมายแยกกันสองแบบ ใน Java มันหมายถึงตัวชี้ แต่ในบริบทของ "Pass-by-Reference" หมายถึงการจัดการกับตัวแปรเดิมที่ผ่านมา
  2. Java เป็นผ่านโดยค่า Java เป็นเชื้อสายของ C (ท่ามกลางภาษาอื่น ๆ ) ก่อนหน้า C ภาษาก่อนหน้านี้หลายภาษา (แต่ไม่ใช่ทั้งหมด) เช่น FORTRAN และ COBOL รองรับ PBR แต่ C ไม่ได้ PBR อนุญาตให้ภาษาอื่นเหล่านี้สามารถเปลี่ยนแปลงตัวแปรที่ส่งผ่านภายในรูทีนย่อย เพื่อที่จะบรรลุสิ่งเดียวกัน (เช่นเปลี่ยนค่าของตัวแปรภายในฟังก์ชั่น) โปรแกรมเมอร์ C ผ่านตัวชี้ไปที่ตัวแปรในฟังก์ชั่น ภาษาที่ได้รับแรงบันดาลใจจาก C เช่น Java ยืมความคิดนี้และส่งต่อตัวชี้ไปยังวิธีการที่ C ทำยกเว้นว่า Java เรียกตัวชี้อ้างอิงของมัน อีกครั้งนี่เป็นการใช้คำว่า "การอ้างอิง" ที่แตกต่างจากใน "การอ้างอิงผ่าน"
  3. C ++ อนุญาตให้มีการอ้างอิงแบบอ้างอิงโดยการประกาศพารามิเตอร์อ้างอิงโดยใช้อักขระ "&" ​​(ซึ่งเป็นอักขระตัวเดียวที่ใช้เพื่อระบุ "ที่อยู่ของตัวแปร" ในทั้ง C และ C ++) ตัวอย่างเช่นถ้าเราส่งผ่านตัวชี้โดยการอ้างอิงพารามิเตอร์และอาร์กิวเมนต์ไม่เพียงชี้ไปที่วัตถุเดียวกัน แต่เป็นตัวแปรเดียวกัน หากมีการตั้งค่าที่อยู่ที่แตกต่างกันหรือเป็นโมฆะอื่น ๆ
  4. ใน C ++ ตัวอย่างด้านล่างฉันผ่านตัวชี้ไปโมฆะยกเลิกสตริงโดยอ้างอิง และในตัวอย่าง Java ด้านล่างฉันผ่านการอ้างอิง Java ไปยัง String (อีกครั้งเหมือนกับตัวชี้ไปยัง String) ตามค่า สังเกตเห็นผลลัพธ์ในข้อคิดเห็น

C ++ ผ่านตัวอย่างการอ้างอิง:

using namespace std;
#include <iostream>

void change (char *&str){   // the '&' makes this a reference parameter
    str = NULL;
}

int main()
{
    char *str = "not Null";
    change(str);
    cout<<"str is " << str;      // ==>str is <null>
}

Java ผ่าน "การอ้างอิง Java" โดยตัวอย่างค่า

public class ValueDemo{

    public void change (String str){
        str = null;
    }

     public static void main(String []args){
        ValueDemo vd = new ValueDemo();
        String str = "not null";
        vd.change(str);
        System.out.println("str is " + str);    // ==> str is not null!!
                                                // Note that if "str" was
                                                // passed-by-reference, it
                                                // WOULD BE NULL after the
                                                // call to change().
     }
}

แก้ไข

หลายคนเขียนความคิดเห็นซึ่งดูเหมือนจะบ่งบอกว่าพวกเขาไม่ได้ดูตัวอย่างของฉันหรือไม่ได้รับตัวอย่าง c ++ ไม่แน่ใจว่าการปลดการเชื่อมต่ออยู่ที่ใด แต่การเดาตัวอย่าง c ++ นั้นไม่ชัดเจน ฉันโพสต์ตัวอย่างเดียวกันเป็นปาสกาลเพราะฉันคิดว่ารหัสผ่านดูดีกว่าปาสกาล แต่ฉันคิดผิด ฉันอาจทำให้ผู้คนสับสนมากขึ้น ฉันหวังว่าไม่

ในปาสคาลพารามิเตอร์ที่ส่งผ่านโดยการอ้างอิงจะเรียกว่า "พารามิเตอร์ var" ในขั้นตอน setToNil ด้านล่างโปรดทราบคำหลัก 'var' ซึ่งอยู่ข้างหน้าพารามิเตอร์ 'ptr' เมื่อมีการชี้ถูกส่งไปยังขั้นตอนนี้ก็จะถูกส่งผ่านโดยการอ้างอิง สังเกตพฤติกรรม: เมื่อโพรซีเดอร์นี้ตั้งค่า ptr เป็นศูนย์ (นั่นเป็นภาษาปาสคาลพูดถึงค่า NULL) มันจะตั้งค่าอาร์กิวเมนต์เป็นศูนย์ - คุณไม่สามารถทำเช่นนั้นใน Java

program passByRefDemo;
type 
   iptr = ^integer;
var
   ptr: iptr;

   procedure setToNil(var ptr : iptr);
   begin
       ptr := nil;
   end;

begin
   new(ptr);
   ptr^ := 10;
   setToNil(ptr);
   if (ptr = nil) then
       writeln('ptr seems to be nil');     { ptr should be nil, so this line will run. }
end.

แก้ไข 2

ข้อความที่ตัดตอนมาบางส่วนจาก"THE Java Programming Language"โดย Ken Arnold, James Gosling (คนที่ประดิษฐ์ Java)และ David Holmes, ตอนที่ 2, ตอนที่ 2.6.5

พารามิเตอร์ทั้งหมดวิธีการจะถูกส่งผ่าน "โดยค่า" กล่าวอีกนัยหนึ่งค่าของตัวแปรพารามิเตอร์ในวิธีการคือสำเนาของผู้เรียกที่ระบุว่าเป็นข้อโต้แย้ง

เขายังคงสร้างประเด็นเดียวกันเกี่ยวกับวัตถุ . .

คุณควรทราบว่าเมื่อพารามิเตอร์การอ้างอิงวัตถุมันเป็นวัตถุอ้างอิงไม่ได้วัตถุเองที่จะถูกส่งผ่านไป "โดยค่า"

และในตอนท้ายของส่วนเดียวกันเขาได้สร้างคำสั่งที่กว้างขึ้นเกี่ยวกับจาวาเป็นเพียงการส่งผ่านค่าและไม่เคยผ่านการอ้างอิง

ภาษาโปรแกรม Java ไม่ผ่านวัตถุโดยการอ้างอิง มัน ผ่านไปอ้างอิงวัตถุโดยค่า เนื่องจากสำเนาสองชุดของการอ้างอิงเดียวกันอ้างถึงวัตถุจริงเดียวกันการเปลี่ยนแปลงที่ทำผ่านตัวแปรอ้างอิงหนึ่งจะปรากฏให้เห็นผ่านอีกชุดหนึ่ง มีพารามิเตอร์หนึ่งตัวที่ผ่านโหมด - ส่งค่าและช่วยให้สิ่งต่าง ๆ ง่ายขึ้น

หนังสือส่วนนี้มีคำอธิบายที่ยอดเยี่ยมเกี่ยวกับการส่งผ่านพารามิเตอร์ใน Java และความแตกต่างระหว่างการอ้างอิงผ่านและการส่งต่อค่าและเป็นผู้สร้าง Java ฉันอยากจะแนะนำให้ทุกคนอ่านโดยเฉพาะถ้าคุณยังไม่มั่นใจ

ฉันคิดว่าความแตกต่างระหว่างทั้งสองรุ่นนั้นบอบบางมากและถ้าคุณไม่ได้เขียนโปรแกรมที่คุณใช้การอ้างอิงแบบอ้างอิงจริงแล้วมันก็ง่ายที่จะพลาดที่สองรุ่นนั้นแตกต่างกัน

ฉันหวังว่าสิ่งนี้จะเป็นที่ถกเถียงกัน แต่อาจจะไม่

แก้ไข 3

ฉันอาจจะหมกมุ่นอยู่กับโพสต์นี้เล็กน้อย อาจเป็นเพราะฉันรู้สึกว่าผู้ผลิต Java กระจายข้อมูลที่ผิดโดยไม่ตั้งใจ หากแทนที่จะใช้คำว่า "การอ้างอิง" สำหรับพอยน์เตอร์ที่พวกเขาใช้อย่างอื่นพูด dingleberry จะไม่มีปัญหา คุณสามารถพูดได้ว่า "Java ผ่าน dingleberries ตามคุณค่าไม่ใช่โดยการอ้างอิง" และไม่มีใครสับสน

นั่นเป็นเหตุผลที่มีเพียงผู้พัฒนา Java มีปัญหากับเรื่องนี้ พวกเขามองไปที่คำว่า "การอ้างอิง" และคิดว่าพวกเขารู้ว่ามันหมายถึงอะไรดังนั้นพวกเขาจึงไม่สนใจที่จะพิจารณาข้อโต้แย้งที่เป็นปฏิปักษ์

อย่างไรก็ตามฉันสังเกตเห็นความคิดเห็นในโพสต์เก่าซึ่งทำให้การเปรียบเทียบบอลลูนที่ฉันชอบจริงๆ มากเหลือเกินที่ฉันตัดสินใจที่จะยึดติดกันกับคลิปอาร์ตเพื่อสร้างการ์ตูนชุดหนึ่งเพื่ออธิบายประเด็น

ผ่านการอ้างอิงโดยค่า - การเปลี่ยนแปลงการอ้างอิงจะไม่สะท้อนในขอบเขตของผู้โทร แต่การเปลี่ยนแปลงของวัตถุนั้น นี่เป็นเพราะการอ้างอิงถูกคัดลอก แต่ทั้งต้นฉบับและการคัดลอกอ้างถึงวัตถุเดียวกัน ผ่านการอ้างอิงวัตถุตามค่า

ผ่านการอ้างอิง -ไม่มีสำเนาของการอ้างอิง การอ้างอิงเดี่ยวถูกแชร์โดยทั้งผู้เรียกและฟังก์ชันที่กำลังเรียกใช้ การเปลี่ยนแปลงใด ๆ ในการอ้างอิงหรือข้อมูลของวัตถุจะปรากฏในขอบเขตของผู้โทร ผ่านการอ้างอิง

แก้ไข 4

ฉันได้เห็นโพสต์ในหัวข้อนี้ซึ่งอธิบายการใช้งานพารามิเตอร์ระดับต่ำใน Java ซึ่งฉันคิดว่าดีและมีประโยชน์มากเพราะทำให้เป็นแนวคิดที่เป็นรูปธรรม อย่างไรก็ตามสำหรับฉันแล้วคำถามเกี่ยวกับพฤติกรรมที่อธิบายในสเปคภาษามากกว่าเกี่ยวกับการใช้เทคนิคของพฤติกรรม นี่คือ exerpt จากข้อกำหนดภาษา Java ส่วน 8.4.1 :

เมื่อมีการเรียกใช้เมธอดหรือคอนสตรัคเตอร์ (§15.12) ค่าของนิพจน์อาร์กิวเมนต์ที่เกิดขึ้นจริงจะเริ่มต้นตัวแปรพารามิเตอร์ที่สร้างขึ้นใหม่แต่ละชนิดที่ประกาศไว้ก่อนการเรียกใช้งานเนื้อความของเมธอดหรือคอนสตรัคเตอร์ ตัวบ่งชี้ที่ปรากฏใน DeclaratorId อาจถูกใช้เป็นชื่อง่าย ๆ ในเนื้อความของเมธอดหรือตัวสร้างเพื่ออ้างถึงพารามิเตอร์ที่เป็นทางการ

ซึ่งหมายความว่า java สร้างสำเนาของพารามิเตอร์ที่ส่งผ่านก่อนที่จะดำเนินการวิธีการ เช่นคนส่วนใหญ่ที่เรียนคอมไพเลอร์ในวิทยาลัยผมใช้"มังกรหนังสือ"ซึ่งเป็นหนังสือคอมไพเลอร์ มันมีคำอธิบายที่ดีของ "Call-by-value" และ "Call-by-Reference" ในบทที่ 1 คำอธิบาย Call-by-value ตรงกับ Java Specs อย่างแน่นอน

ย้อนกลับไปเมื่อฉันศึกษาคอมไพเลอร์ - ในยุค 90 ฉันใช้หนังสือเล่มแรกของปี 1986 ที่ชวาล่วงหน้าประมาณ 9 หรือ 10 ปี อย่างไรก็ตามฉันเพิ่งเจอสำเนาของEddition ที่ 2จากปี 2007 ซึ่งจริงๆแล้วพูดถึง Java! ส่วนที่ 1.6.6 ระบุว่า "กลไกการส่งผ่านพารามิเตอร์" อธิบายพารามิเตอร์ที่ส่งผ่านได้ค่อนข้างดี นี่คือข้อความที่ตัดตอนมาภายใต้หัวข้อ "Call-by-value" ซึ่งกล่าวถึง Java:

ใน call-by-value พารามิเตอร์จริงจะถูกประเมิน (ถ้าเป็นนิพจน์) หรือคัดลอก (หากเป็นตัวแปร) ค่าจะถูกวางในตำแหน่งที่เป็นของพารามิเตอร์ทางการที่สอดคล้องกันของขั้นตอนที่เรียกว่า วิธีนี้ใช้ใน C และ Java และเป็นตัวเลือกทั่วไปใน C ++ รวมถึงในภาษาอื่น ๆ ส่วนใหญ่


4
จริงๆแล้วคุณสามารถทำให้คำตอบนี้ง่ายขึ้นโดยบอกว่า Java ผ่านค่าสำหรับประเภทดั้งเดิมเท่านั้น ทุกสิ่งที่สืบทอดจาก Object นั้นผ่านการอ้างอิงอย่างมีประสิทธิภาพโดยที่การอ้างอิงคือตัวชี้ที่คุณกำลังผ่าน
Scuba Steve

1
@JuanMendes ฉันเพิ่งใช้ C ++ เป็นตัวอย่าง ฉันจะยกตัวอย่างภาษาอื่นให้คุณ คำว่า "ผ่านการอ้างอิง" มีอยู่นานก่อนที่จะมี C ++ มันเป็นคำศัพท์ในตำราที่มีคำจำกัดความเฉพาะเจาะจงมาก และโดยนิยามแล้ว Java ไม่ผ่านการอ้างอิง คุณสามารถใช้คำนี้ต่อไปได้หากคุณต้องการ แต่การใช้งานของคุณจะไม่สอดคล้องกับคำนิยามของตำราเรียน มันไม่ใช่แค่ตัวชี้ไปยังตัวชี้ มันเป็นโครงสร้างที่จัดทำโดยภาษาเพื่ออนุญาตให้ "ส่งผ่านข้อมูลอ้างอิง" โปรดลองดูตัวอย่างของฉันอย่างใกล้ชิด แต่โปรดอย่าใช้คำของฉันและดูด้วยตัวคุณเอง
Sanjeev

2
@Automated ฉันคิดว่าการอธิบาย Java ว่า "pass-by-reference" นั้นทำให้เข้าใจผิดการอ้างอิงของพวกเขานั้นไม่มีอะไรมากไปกว่าพอยน์เตอร์ ตามที่ Sanjeev เขียนค่าการอ้างอิงและตัวชี้เป็นคำศัพท์ตำราที่มีความหมายของตัวเองโดยไม่คำนึงถึงสิ่งที่ผู้สร้าง Java ใช้
Jędrzej Dudkiewicz

1
@ ArtanisZeratul จุดนั้นบอบบาง แต่ไม่ซับซ้อน โปรดดูการ์ตูนที่ฉันโพสต์ไว้ ฉันรู้สึกว่ามันง่ายที่จะติดตาม Java นั้นเป็นแบบ Pass-by-value ไม่ได้เป็นเพียงความคิดเห็น มันเป็นความจริงตามคำนิยามในตำราเรียนของการส่งผ่านค่า นอกจากนี้ "คนที่มักจะพูดว่าผ่านคุณค่า" รวมถึงนักวิทยาศาสตร์คอมพิวเตอร์เช่น James Gosling ผู้สร้าง Java โปรดดูคำพูดจากหนังสือของเขาภายใต้ "แก้ไข 2" ในโพสต์ของฉัน
Sanjeev

1
คำตอบที่ยอดเยี่ยมคำว่า "การส่งต่ออ้างอิง" ไม่มีอยู่ใน java (และภาษาอื่น ๆ เช่น JS)
newfolder

56

เท่าที่ฉันรู้, 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 ซึ่งไม่มีผลกระทบกับการอ้างอิงในหลัก แต่ถ้าวัตถุของคุณไม่เปลี่ยนรูปคุณสามารถเปลี่ยนได้เช่น:

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

อย่างไรก็ตามคุณสามารถสร้าง wrapper สำหรับ String เช่นนี้ซึ่งจะทำให้สามารถใช้กับ Strings ได้:

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 เมื่อมันมาถึง "เพิ่ม" สอง Strings เพราะคุณสามารถปรับเปลี่ยนวัตถุต้นฉบับที่คุณไม่สามารถมีวัตถุที่ไม่เปลี่ยนรูปเช่น String เป็น


+1 สำหรับการทดสอบแลกเปลี่ยน - น่าจะเป็นวิธีที่ง่ายที่สุดและ relatable ที่จะแยกแยะระหว่างผ่านโดยการอ้างอิงและผ่านการอ้างอิงโดยค่า iff คุณสามารถเขียนฟังก์ชันswap(a, b)ว่า (1) สัญญาแลกเปลี่ยนaและbจากที่โทรมา POV (2) เป็นชนิดไม่เชื่อเรื่องพระเจ้าขอบเขตที่พิมพ์คงช่วย (หมายถึงใช้กับอีกประเภทหนึ่งต้องมีอะไรมากไปกว่าการเปลี่ยนประเภทการประกาศaและb) และ (3) ไม่ต้องการให้ผู้โทรผ่านตัวชี้หรือชื่ออย่างชัดเจนจากนั้นภาษารองรับการส่งต่อโดยการอ้างอิง
cHao

".. สำหรับประเภทข้อมูลดั้งเดิมคุณจะทำงานกับสำเนาและสำหรับวัตถุที่คุณจะทำงานกับสำเนาของการอ้างอิงไปยังวัตถุ" - เขียนอย่างสมบูรณ์!
Raúl

55

ไม่มันไม่ผ่านการอ้างอิง

Java ผ่านค่าตามข้อกำหนดภาษา Java:

เมื่อมีการเรียกใช้เมธอดหรือคอนสตรัคเตอร์ (§15.12) ค่าของนิพจน์อาร์กิวเมนต์ที่เกิดขึ้นจริงจะเริ่มต้นตัวแปรพารามิเตอร์ที่สร้างขึ้นใหม่แต่ละชนิดที่ประกาศไว้ก่อนการเรียกใช้งานเนื้อความของเมธอดหรือคอนสตรัคเตอร์ ตัวบ่งชี้ที่ปรากฏใน DeclaratorId อาจจะใช้เป็นชื่อที่ง่ายในร่างกายของวิธีการหรือสร้างเพื่ออ้างถึงพารามิเตอร์อย่างเป็นทางการ


52

ให้ฉันพยายามอธิบายความเข้าใจของฉันด้วยความช่วยเหลือจากสี่ตัวอย่าง 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:

/ ** นี่คือ 'Pass By Value มีความรู้สึกของ' Pass By Reference '

บางคนบอกว่าประเภทดั้งเดิมและ 'สตริง' คือ 'ผ่านค่า' และวัตถุคือ 'ผ่านการอ้างอิง'

แต่จากตัวอย่างนี้เราสามารถเข้าใจได้ว่ามันเป็นค่า infact pass by value เท่านั้นโดยจำไว้ว่าที่นี่เราผ่านการอ้างอิงเป็นค่า 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:

/ **

นอกจากสิ่งที่ถูกกล่าวถึงในตัวอย่างที่ 3 (PassByValueObjectCase1.java) เราไม่สามารถเปลี่ยนการอ้างอิงจริงนอกขอบเขตเดิม "

หมายเหตุ: private class Studentผมไม่ได้วางโค้ดสำหรับ คำจำกัดความของคลาสสำหรับStudentเช่นเดียวกับตัวอย่าง 3

* /

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]

49

คุณไม่สามารถส่งต่อโดยอ้างอิงใน Java และหนึ่งในวิธีที่เห็นได้ชัดคือเมื่อคุณต้องการคืนค่ามากกว่าหนึ่งค่าจากการเรียกใช้เมธอด พิจารณารหัสต่อไปนี้ใน C ++:

void getValues(int& arg1, int& arg2) {
    arg1 = 1;
    arg2 = 2;
}
void caller() {
    int x;
    int y;
    getValues(x, y);
    cout << "Result: " << x << " " << y << endl;
}

บางครั้งคุณต้องการใช้รูปแบบเดียวกันใน Java แต่คุณทำไม่ได้ อย่างน้อยก็โดยตรง คุณสามารถทำสิ่งนี้แทน:

void getValues(int[] arg1, int[] arg2) {
    arg1[0] = 1;
    arg2[0] = 2;
}
void caller() {
    int[] x = new int[1];
    int[] y = new int[1];
    getValues(x, y);
    System.out.println("Result: " + x[0] + " " + y[0]);
}

ตามที่ได้อธิบายไว้ในคำตอบก่อนใน Java getValuesคุณผ่านตัวชี้ไปยังอาร์เรย์เป็นค่าลง นั่นก็เพียงพอแล้วเพราะวิธีการนั้นจะแก้ไของค์ประกอบของอาร์เรย์และโดยการประชุมคุณคาดหวังว่าองค์ประกอบ 0 จะมีค่าส่งคืน เห็นได้ชัดว่าคุณสามารถทำได้ในวิธีอื่นเช่นการสร้างโค้ดของคุณดังนั้นจึงไม่จำเป็นหรือสร้างคลาสที่สามารถมีค่าส่งคืนหรืออนุญาตให้ตั้งค่า แต่รูปแบบง่าย ๆ ที่มีให้คุณใน C ++ ด้านบนนั้นไม่มีใน Java


48

ฉันคิดว่าฉันสนับสนุนคำตอบนี้เพื่อเพิ่มรายละเอียดเพิ่มเติมจากข้อมูลจำเพาะ

อย่างแรกความแตกต่างระหว่างการส่งผ่านโดยอ้างอิงกับการผ่านตามค่าคืออะไร

การส่งต่อโดยการอ้างอิงหมายถึงพารามิเตอร์ที่เรียกว่าฟังก์ชั่น 'จะเหมือนกับอาร์กิวเมนต์ที่ส่งผ่านของผู้โทร (ไม่ใช่ค่า แต่เป็นข้อมูลเฉพาะตัว - ตัวแปรเอง)

Pass by value หมายถึงพารามิเตอร์ที่เรียกว่าฟังก์ชั่น 'จะเป็นสำเนาของอาร์กิวเมนต์ที่ส่งผ่านของผู้โทร

หรือจากวิกิพีเดียในเรื่องของการอ้างอิงโดยผ่าน

ในการประเมินผลการโทรโดยอ้างอิง (หรือที่เรียกว่าการส่งต่อโดยอ้างอิง) ฟังก์ชันจะได้รับการอ้างอิงโดยนัยไปยังตัวแปรที่ใช้เป็นอาร์กิวเมนต์แทนที่จะเป็นสำเนาของค่า โดยทั่วไปหมายความว่าฟังก์ชันสามารถแก้ไข (เช่นกำหนดให้) ตัวแปรที่ใช้เป็นอาร์กิวเมนต์ซึ่งเป็นสิ่งที่ผู้โทรจะเห็น

และในเรื่องของการส่งต่อค่า

ใน call-by-value นิพจน์อาร์กิวเมนต์จะถูกประเมินและค่าผลลัพธ์จะถูกโยงกับตัวแปรที่เกี่ยวข้องในฟังก์ชัน [... ] หากฟังก์ชั่นหรือขั้นตอนสามารถกำหนดค่าให้กับพารามิเตอร์ของมันจะมีการกำหนดเฉพาะสำเนาภายใน [... ]

ประการที่สองเราจำเป็นต้องรู้ว่า Java ใช้อะไรในการเรียกใช้เมธอด Java Language ข้อกำหนดรัฐ

เมื่อมีการเรียกใช้เมธอดหรือคอนสตรัคเตอร์ (§15.12) ค่าของนิพจน์อาร์กิวเมนต์ที่เกิดขึ้นจริงจะเริ่มต้นตัวแปรพารามิเตอร์ที่สร้างขึ้นใหม่แต่ละชนิดที่ประกาศไว้ก่อนการเรียกใช้งานเนื้อความของเมธอดหรือคอนสตรัคเตอร์

ดังนั้นมันจึงกำหนด (หรือผูก) ค่าของอาร์กิวเมนต์ให้กับตัวแปรพารามิเตอร์ที่เกี่ยวข้อง

ค่าของการโต้แย้งคืออะไร?

ลองพิจารณาประเภทอ้างอิงJava เครื่องเสมือนจำเพาะรัฐ

ประเภทการอ้างอิงมีสามประเภทได้แก่ ประเภทคลาสประเภทอาร์เรย์และประเภทอินเตอร์เฟส ค่าของพวกเขาคือการอ้างอิงถึงอินสแตนซ์คลาสที่สร้างขึ้นแบบไดนามิกอาร์เรย์หรืออินสแตนซ์ของคลาสหรืออาร์เรย์ที่ใช้อินเทอร์เฟซตามลำดับ

Java Language ข้อกำหนดยังระบุ

ค่าอ้างอิง (มักจะอ้างอิงเพียง) เป็นตัวชี้ไปยังวัตถุเหล่านี้และการอ้างอิงโมฆะพิเศษซึ่งหมายถึงไม่มีวัตถุ

ค่าของการโต้แย้ง (บางประเภทอ้างอิง) เป็นตัวชี้ไปยังวัตถุ โปรดทราบว่าตัวแปรการเรียกใช้เมธอดที่มีชนิดการคืนค่าชนิดการอ้างอิงและนิพจน์การสร้างอินสแตนซ์ ( new ...) ทั้งหมดจะแก้ไขค่าประเภทการอ้างอิง

ดังนั้น

public void method (String param) {}
...
String var = new String("ref");
method(var);
method(var.toString());
method(new String("ref"));

ทั้งหมดผูกค่าของการอ้างอิงถึงเป็นตัวอย่างให้กับพารามิเตอร์ที่สร้างขึ้นใหม่ของวิธีการString paramนี่คือสิ่งที่คำจำกัดความของรหัสผ่านตามค่าอธิบาย เช่นJava เป็นผ่านโดยค่า

ความจริงที่ว่าคุณสามารถติดตามการอ้างอิงเพื่อเรียกใช้เมธอดหรือเข้าถึงฟิลด์ของวัตถุที่อ้างอิงนั้นไม่เกี่ยวข้องกับการสนทนาอย่างสมบูรณ์ คำจำกัดความของการอ้างอิงผ่านคือ

โดยทั่วไปหมายความว่าฟังก์ชันสามารถแก้ไข (เช่นกำหนดให้) ตัวแปรที่ใช้เป็นอาร์กิวเมนต์ซึ่งเป็นสิ่งที่ผู้โทรจะเห็น

ใน Java การแก้ไขตัวแปรหมายถึงการกำหนดใหม่ ใน Java หากคุณกำหนดตัวแปรใหม่ภายในเมธอดมันจะไม่ถูกสังเกตเห็นถึงผู้เรียก การปรับเปลี่ยนวัตถุที่อ้างอิงโดยตัวแปรนั้นเป็นแนวคิดที่แตกต่างอย่างสิ้นเชิง


ค่าดั้งเดิมมีการกำหนดไว้ยังอยู่ใน Java เครื่องเสมือนข้อมูลจำเพาะที่นี่ ค่าของประเภทคือค่าจุดอินทิกรัลหรือทศนิยมที่สอดคล้องกัน, เข้ารหัสอย่างเหมาะสม (บิต 8, 16, 32, 64, ฯลฯ )


42

ใน Java การอ้างอิงเท่านั้นที่ถูกส่งผ่านและถูกส่งผ่านโดยค่า:

อาร์กิวเมนต์ Java ถูกส่งผ่านโดยค่าทั้งหมด (การอ้างอิงจะถูกคัดลอกเมื่อใช้โดยวิธีการ):

ในกรณีของประเภทดั้งเดิมพฤติกรรมของ Java นั้นง่าย: ค่าจะถูกคัดลอกในอินสแตนซ์อื่นของประเภทดั้งเดิม

ในกรณีของ Object สิ่งนี้จะเหมือนกัน: ตัวแปร Object คือพอยน์เตอร์ (ที่เก็บข้อมูล) ที่เก็บเฉพาะที่อยู่ของวัตถุที่สร้างขึ้นโดยใช้คำหลัก "ใหม่" และคัดลอกเช่นประเภทดั้งเดิม

ลักษณะการทำงานอาจแตกต่างจากชนิดดั้งเดิม: เนื่องจากตัวแปรวัตถุที่คัดลอกมีที่อยู่เดียวกัน (ไปยังวัตถุเดียวกัน) เนื้อหา / สมาชิกของวัตถุอาจยังคงถูกแก้ไขภายในเมธอดและเข้าถึงในภายหลังโดยให้ภาพลวงตาว่าวัตถุ (บรรจุ) ตัวเองถูกส่งผ่านโดยการอ้างอิง

"สตริง" วัตถุที่ดูเหมือนจะเป็นสิ่งที่ดีที่เคาน์เตอร์ตัวอย่างกับตำนานเมืองกล่าวว่า "วัตถุที่ถูกส่งผ่านโดยการอ้างอิง":

ผลโดยใช้วิธีการคุณจะไม่สามารถอัปเดตค่าของสตริงที่ส่งเป็นอาร์กิวเมนต์:

วัตถุสตริงถือตัวละครโดยอาร์เรย์ประกาศสุดท้ายที่ไม่สามารถแก้ไขได้ ที่อยู่ของวัตถุอาจถูกแทนที่ด้วยที่อยู่อื่นโดยใช้ "ใหม่" การใช้ "ใหม่" เพื่ออัปเดตตัวแปรจะไม่อนุญาตให้เข้าถึงวัตถุจากด้านนอกเนื่องจากตัวแปรถูกส่งผ่านครั้งแรกด้วยค่าและคัดลอก


ดังนั้นมันเป็นเรื่องเกี่ยวกับวัตถุและ byVal เกี่ยวกับวัตถุโบราณหรือไม่?
Mox

@mox โปรดอ่าน: วัตถุไม่ถูกส่งผ่านโดยการอ้างอิงนี่คือ ledgend: String a = new String ("ไม่เปลี่ยนแปลง");
user1767316

1
@Aaron "Pass by Reference" ไม่ได้หมายความว่า "ส่งค่าที่เป็นสมาชิกประเภทที่เรียกว่า 'การอ้างอิง' ใน Java" การใช้ "อ้างอิง" สองครั้งหมายถึงสิ่งต่าง ๆ
philipxy

2
คำตอบที่ค่อนข้างสับสน เหตุผลที่คุณไม่สามารถเปลี่ยนวัตถุสตริงในวิธีการซึ่งจะถูกส่งโดยการอ้างอิงคือว่าวัตถุ String strParam.setChar ( i, newValue )จะไม่เปลี่ยนรูปโดยการออกแบบและคุณไม่สามารถทำสิ่งที่ชอบ ที่กล่าวไว้สตริงจะถูกส่งผ่านโดยค่าและเนื่องจาก String เป็นชนิดที่ไม่ใช่แบบดั้งเดิมค่านั้นจึงเป็นการอ้างอิงถึงสิ่งที่สร้างขึ้นด้วยใหม่และคุณสามารถตรวจสอบได้โดยใช้ String.intern () .
zakmck

1
แต่คุณไม่สามารถเปลี่ยนสตริงผ่าน param = "สตริงอื่น" (เทียบเท่ากับสตริงใหม่ ("สตริงอื่น")) เนื่องจากค่าอ้างอิงของพารามิเตอร์ (ซึ่งตอนนี้ชี้ไปที่ "สตริงอื่น") ไม่สามารถกลับมาจากวิธีการได้ ร่างกาย. แต่นั่นเป็นความจริงสำหรับวัตถุอื่น ๆ ความแตกต่างก็คือเมื่ออินเตอร์เฟสคลาสอนุญาตให้ทำได้คุณสามารถทำ param.changeMe () และวัตถุระดับบนสุดที่ถูกเรียกจะเปลี่ยนเพราะ Param ชี้ไปที่มันแม้จะมีค่าอ้างอิงของพารามิเตอร์เองก็ตาม (ตำแหน่งที่ระบุไว้ในข้อกำหนดของ C) ไม่สามารถแสดงป๊อปอัปกลับจากวิธีการได้
zakmck

39

ความแตกต่างหรือบางทีอาจเป็นแค่วิธีที่ฉันจำได้เมื่อฉันเคยอยู่ภายใต้ความประทับใจเช่นเดียวกับโปสเตอร์ต้นฉบับคือ: Java มักผ่านคุณค่า วัตถุทั้งหมด (ใน Java สิ่งใด ๆ ยกเว้นดั้งเดิม) ใน Java เป็นการอ้างอิง การอ้างอิงเหล่านี้ถูกส่งผ่านตามค่า


2
ฉันพบว่าประโยคที่สองถึงครั้งสุดท้ายของคุณทำให้เข้าใจผิดมาก ไม่เป็นความจริงเลยว่า "วัตถุทั้งหมดใน Java เป็นข้อมูลอ้างอิง" มันเป็นเพียงการอ้างอิงไปยังวัตถุเหล่านั้นที่มีการอ้างอิง
Dawood ibn Kareem

37

อย่างที่หลาย ๆ คนพูดถึงก่อนหน้านี้Java มักจะผ่านคุณค่าอยู่เสมอ

นี่เป็นอีกตัวอย่างที่จะช่วยให้คุณเข้าใจความแตกต่าง ( ตัวอย่างการสลับแบบคลาสสิค ):

public class Test {
  public static void main(String[] args) {
    Integer a = new Integer(2);
    Integer b = new Integer(3);
    System.out.println("Before: a = " + a + ", b = " + b);
    swap(a,b);
    System.out.println("After: a = " + a + ", b = " + b);
  }

  public static swap(Integer iA, Integer iB) {
    Integer tmp = iA;
    iA = iB;
    iB = tmp;
  }
}

พิมพ์:

ก่อนหน้านี้: a = 2, b = 3
หลัง: a = 2, b = 3

สิ่งนี้เกิดขึ้นเพราะ iA และ iB เป็นตัวแปรอ้างอิงท้องถิ่นใหม่ที่มีค่าเดียวกันของการอ้างอิงที่ส่งผ่าน (พวกเขาชี้ไปที่ a และ b ตามลำดับ) ดังนั้นการพยายามเปลี่ยนการอ้างอิงของ iA หรือ iB จะเปลี่ยนเฉพาะในขอบเขตภายในเท่านั้นและไม่ได้อยู่นอกวิธีการนี้


32

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();
}

4
นี่เป็นวิธีที่ชัดเจนและง่ายที่สุดที่จะเห็นว่า Java ส่งผ่านค่า ค่าของobj( null) ถูกส่งผ่านไปยังไม่ได้มีการอ้างอิงถึงinit obj
David Schwartz

31

ฉันมักจะคิดว่ามันเป็น "ส่งสำเนา" มันเป็นสำเนาของค่าไม่ว่าจะเป็นแบบดั้งเดิมหรือการอ้างอิง ถ้ามันเป็นแบบดั้งเดิมมันเป็นสำเนาของบิตที่มีค่าและถ้ามันเป็นวัตถุมันเป็นสำเนาของการอ้างอิง

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

คลาส wrapper ดั้งเดิมและ Strings นั้นไม่เปลี่ยนรูปดังนั้นตัวอย่างใด ๆ ที่ใช้ชนิดเหล่านั้นจะไม่ทำงานเหมือนกับประเภท / วัตถุอื่น ๆ


5
"ผ่านการคัดลอก" คือสิ่งที่ผ่านมาโดยมีมูลค่าหมายถึง
TJ Crowder

@TJCrowder "Pass by copy" อาจเป็นวิธีที่เหมาะสมกว่าในการแสดงแนวคิดเดียวกันสำหรับวัตถุประสงค์ของ Java: เนื่องจากความหมายทำให้ยากที่ shoehorn ในความคิดคล้ายกับการส่งผ่านการอ้างอิงซึ่งเป็นสิ่งที่คุณต้องการใน Java
ไมค์หนู

@mikerodent - ขออภัยฉันไม่ได้ติดตาม :-) "Pass-by-value" และ "Pass-by-Reference" เป็นคำศัพท์ที่ถูกต้องสำหรับแนวคิดเหล่านี้ เราควรใช้พวกเขา (ให้คำจำกัดความของพวกเขาเมื่อผู้คนต้องการพวกเขา) แทนที่จะสร้างสิ่งใหม่ ข้างต้นฉันพูดว่า "การคัดลอก" คือความหมายของ "pass-by-value" แต่จริงๆแล้วมันไม่ชัดเจนว่าการส่งผ่านสำเนาหมายถึงอะไรสำเนาของอะไร มูลค่า? (เช่นการอ้างอิงวัตถุ) วัตถุเอง? ดังนั้นฉันจึงยึดมั่นในข้อกำหนดของศิลปะ :-)
TJ Crowder

28

ไม่เหมือนกับภาษาอื่น ๆ Java ไม่อนุญาตให้คุณเลือกระหว่าง pass-by-value และ pass-by-reference - อาร์กิวเมนต์ทั้งหมดถูกส่งผ่านโดยค่า การเรียกใช้เมธอดสามารถส่งค่าสองประเภทไปยังเมธอดคือสำเนาของค่าดั้งเดิม (เช่นค่าของ int และ double) และสำเนาของการอ้างอิงไปยังวัตถุ

เมื่อวิธีการแก้ไขพารามิเตอร์ชนิดดั้งเดิมการเปลี่ยนแปลงพารามิเตอร์จะไม่มีผลกับค่าอาร์กิวเมนต์ดั้งเดิมในวิธีการเรียก

เมื่อพูดถึงวัตถุวัตถุจะไม่สามารถส่งผ่านไปยังวิธีการต่างๆได้ ดังนั้นเราจึงผ่านการอ้างอิง (ที่อยู่) ของวัตถุ เราสามารถจัดการวัตถุต้นฉบับโดยใช้การอ้างอิงนี้

Java สร้างและเก็บรักษาวัตถุอย่างไร:เมื่อเราสร้างวัตถุเราเก็บที่อยู่ของวัตถุในตัวแปรอ้างอิง ลองวิเคราะห์ข้อความต่อไปนี้

Account account1 = new Account();

“ บัญชี account1” เป็นประเภทและชื่อของตัวแปรอ้างอิง“ =” เป็นผู้ดำเนินการที่ได้รับมอบหมาย“ ใหม่” จะถามถึงจำนวนเนื้อที่ที่ต้องการจากระบบ ตัวสร้างทางด้านขวาของคำหลักใหม่ซึ่งสร้างวัตถุนั้นเรียกว่าโดยนัยโดยคำหลักใหม่ ที่อยู่ของวัตถุที่สร้างขึ้น (ผลลัพธ์ของค่าที่ถูกต้องซึ่งเป็นนิพจน์ที่เรียกว่า "คลาสการสร้างอินสแตนซ์การสร้างนิพจน์") ถูกกำหนดให้กับค่าด้านซ้าย (ซึ่งเป็นตัวแปรอ้างอิงที่มีชื่อและชนิดที่ระบุ) โดยใช้ตัวดำเนินการมอบหมาย

แม้ว่าการอ้างอิงของวัตถุจะถูกส่งผ่านโดยค่าวิธีการยังสามารถโต้ตอบกับวัตถุที่อ้างอิงโดยการเรียกวิธีการสาธารณะโดยใช้สำเนาของการอ้างอิงของวัตถุ เนื่องจากการอ้างอิงที่เก็บไว้ในพารามิเตอร์เป็นสำเนาของการอ้างอิงที่ส่งผ่านเป็นอาร์กิวเมนต์พารามิเตอร์ในเมธอดที่เรียกใช้และอาร์กิวเมนต์ในเมธอดการเรียกอ้างถึงวัตถุเดียวกันในหน่วยความจำ

การส่งผ่านการอ้างอิงไปยังอาร์เรย์แทนที่จะเป็นวัตถุอาร์เรย์เองนั้นสมเหตุสมผลสำหรับเหตุผลด้านประสิทธิภาพ เนื่องจากทุกอย่างใน Java ถูกส่งผ่านโดยค่าถ้าวัตถุอาร์เรย์ถูกส่งผ่านสำเนาของแต่ละองค์ประกอบจะถูกส่งผ่าน สำหรับอาร์เรย์ขนาดใหญ่สิ่งนี้จะเสียเวลาและใช้พื้นที่เก็บข้อมูลจำนวนมากสำหรับสำเนาขององค์ประกอบ

ในภาพด้านล่างคุณจะเห็นว่าเรามีตัวแปรอ้างอิงสองตัว (สิ่งเหล่านี้เรียกว่าพอยน์เตอร์ใน C / C ++ และฉันคิดว่าคำนี้ทำให้เข้าใจคุณลักษณะนี้ได้ง่ายขึ้น) ในวิธีการหลัก ตัวแปรดั้งเดิมและตัวแปรอ้างอิงจะถูกเก็บไว้ในหน่วยความจำสแต็ค (ด้านซ้ายในภาพด้านล่าง) ตัวแปรอ้างอิง array1 และ array2 "จุด" (ในขณะที่โปรแกรมเมอร์ C / C ++ เรียกมันว่า) หรืออ้างอิงไปยังอาร์เรย์ a และ b ตามลำดับซึ่งเป็นวัตถุ (ค่าตัวแปรอ้างอิงเหล่านี้ถือเป็นที่อยู่ของวัตถุ) ในหน่วยความจำฮีป (ด้านขวาในภาพด้านล่าง) .

ผ่านค่าตัวอย่าง 1

ถ้าเราส่งค่าของตัวแปรอ้างอิง array1 เป็นอาร์กิวเมนต์ไปยังวิธี reverseArray ตัวแปรอ้างอิงจะถูกสร้างขึ้นในวิธีการและตัวแปรอ้างอิงนั้นเริ่มชี้ไปที่อาร์เรย์เดียวกัน (a)

public class Test
{
    public static void reverseArray(int[] array1)
    {
        // ...
    }

    public static void main(String[] args)
    {
        int[] array1 = { 1, 10, -7 };
        int[] array2 = { 5, -190, 0 };

        reverseArray(array1);
    }
}

ผ่านค่าตัวอย่าง 2

ดังนั้นถ้าเราพูด

array1[0] = 5;

ในวิธี reverseArray มันจะทำการเปลี่ยนแปลงในอาร์เรย์

เรามีตัวแปรอ้างอิงอีกอันในวิธี reverseArray (array2) ที่ชี้ไปยังอาร์เรย์ c ถ้าเราจะพูด

array1 = array2;

ในเมธอด reverseArray ดังนั้นตัวแปรอ้างอิง array1 ในเมธอด reverseArray จะหยุดชี้ไปที่อาร์เรย์ a และเริ่มชี้ไปที่อาร์เรย์ c (เส้นประในรูปภาพที่สอง)

ถ้าเราคืนค่าตัวแปรอ้างอิง array2 เป็นค่าส่งคืนของ method reverseArray และกำหนดค่านี้ให้กับตัวแปรอ้างอิง array1 ในวิธีการหลัก array1 ใน main จะเริ่มชี้ไปที่อาร์เรย์ c

งั้นลองเขียนทุกสิ่งที่เราทำไปทันที

public class Test
{
    public static int[] reverseArray(int[] array1)
    {
        int[] array2 = { -7, 0, -1 };

        array1[0] = 5; // array a becomes 5, 10, -7

        array1 = array2; /* array1 of reverseArray starts
          pointing to c instead of a (not shown in image below) */
        return array2;
    }

    public static void main(String[] args)
    {
        int[] array1 = { 1, 10, -7 };
        int[] array2 = { 5, -190, 0 };

        array1 = reverseArray(array1); /* array1 of 
         main starts pointing to c instead of a */
    }
}

ป้อนคำอธิบายรูปภาพที่นี่

และตอนนี้วิธี reverseArray สิ้นสุดลงแล้วตัวแปรอ้างอิงของมัน (array1 และ array2) หายไป ซึ่งหมายความว่าตอนนี้เรามีตัวแปรอ้างอิงสองตัวเท่านั้นในเมธอด main array1 และ array2 ซึ่งชี้ไปที่อาร์เรย์ c และ b ตามลำดับ ไม่มีตัวแปรอ้างอิงที่ชี้ไปยังวัตถุ (อาร์เรย์) ดังนั้นจึงมีสิทธิ์ได้รับการเก็บขยะ

คุณสามารถกำหนดค่าของ array2 ใน main ให้กับ array1 ได้ array1 จะเริ่มชี้ไปที่ b


27

ผมได้สร้างด้ายที่ทุ่มเทให้กับชนิดของคำถามเหล่านี้สำหรับการใด ๆการเขียนโปรแกรมภาษาที่นี่

Java นอกจากนี้ยังกล่าวถึง นี่คือบทสรุปสั้น ๆ :

  • Java ผ่านพารามิเตอร์ตามค่า
  • "ตามค่า" เป็นวิธีเดียวใน java ที่จะส่งผ่านพารามิเตอร์ไปยังวิธีการ
  • การใช้วิธีการจากวัตถุที่กำหนดเป็นพารามิเตอร์จะเปลี่ยนวัตถุเป็นจุดอ้างอิงไปยังวัตถุต้นฉบับ (ถ้าวิธีนั้นเปลี่ยนแปลงค่าบางอย่าง)

27

เพื่อให้เนื้อเรื่องสั้น ๆวัตถุJavaมีคุณสมบัติที่แปลกประหลาดมาก

โดยทั่วไป, Java มีชนิดดั้งเดิม ( int, bool, char, doubleฯลฯ ) ที่จะถูกส่งผ่านโดยตรงโดยค่า จากนั้น Java มีวัตถุ (ทุกอย่างที่เกิดขึ้นจากjava.lang.Object) จริง ๆ แล้ววัตถุได้รับการจัดการผ่านการอ้างอิง (การอ้างอิงเป็นตัวชี้ที่คุณไม่สามารถสัมผัสได้) นั่นหมายความว่าวัตถุจะถูกส่งผ่านโดยการอ้างอิงตามปกติแล้วการอ้างอิงจะไม่น่าสนใจ อย่างไรก็ตามหมายความว่าคุณไม่สามารถเปลี่ยนวัตถุที่ชี้ไปตามการอ้างอิงตัวเองผ่านค่า

ฟังดูแปลกและสับสนไหม ลองพิจารณาวิธีที่ C ดำเนินการผ่านการอ้างอิงและการส่งผ่านค่า ใน C อนุสัญญาเริ่มต้นคือการผ่านตามค่า void foo(int x)ผ่าน int โดยค่า void foo(int *x)เป็นฟังก์ชั่นที่ไม่ต้องการint aแต่ชี้ไปยัง int foo(&a)A: ใครจะใช้สิ่งนี้กับ&ผู้ประกอบการที่จะผ่านที่อยู่ตัวแปร

นำสิ่งนี้ไปที่ C ++ และเรามีการอ้างอิง การอ้างอิงโดยทั่วไปแล้ว (ในบริบทนี้) น้ำตาลทรายที่ซ่อนส่วนชี้ของสมการ: void foo(int &x)เรียกโดยfoo(a)ที่ผู้แปลเองรู้ว่ามันเป็นการอ้างอิงและที่อยู่ของการอ้างอิงที่aไม่ควรจะถูกส่งผ่าน ใน Java ตัวแปรทั้งหมดที่อ้างอิงถึงออบเจ็กต์เป็นชนิดอ้างอิงจริงโดยมีผลบังคับให้มีการโทรโดยการอ้างอิงสำหรับจุดประสงค์และจุดประสงค์ส่วนใหญ่โดยไม่ต้องมีการควบคุมที่ละเอียด (และความซับซ้อน) เช่น C ++


ฉันคิดว่ามันใกล้เคียงกับความเข้าใจเกี่ยวกับวัตถุ Java และการอ้างอิงของมันมาก วัตถุใน Java ถูกส่งผ่านไปยังวิธีการโดยคัดลอกอ้างอิง (หรือนามแฝง)
MaxZoom
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.