Java เป็น "pass-by-reference" หรือ "pass-by-value"?
ผมคิดเสมอว่าการใช้งาน Java ผ่านโดยการอ้างอิง
อย่างไรก็ตามฉันเคยเห็นบล็อกโพสต์สองสามรายการ (เช่นบล็อกนี้ ) ที่อ้างว่าไม่ใช่ (บล็อกโพสต์บอกว่า Java ใช้pass-by-value )
ฉันไม่คิดว่าฉันเข้าใจความแตกต่างที่พวกเขาสร้างขึ้น
มีคำอธิบายอย่างไร?
คำตอบ
Java เป็นpass-by-valueเสมอ
แต่น่าเสียดายที่เราไม่เคยจัดการกับวัตถุที่ทุกแทนเล่นกลจับวัตถุที่เรียกว่าการอ้างอิง (ซึ่งจะถูกส่งผ่านโดยค่าของหลักสูตร) คำศัพท์และความหมายที่เลือกได้ง่ายทำให้ผู้เริ่มต้นหลายคนสับสน
มันจะเป็นแบบนี้:
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
เองได้
ฉันเพิ่งสังเกตเห็นว่าคุณอ้างถึงบทความของฉัน
Java Spec บอกว่าทุกอย่างใน Java เป็นแบบ pass-by-value ไม่มีสิ่งที่เรียกว่า "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
- someDog ตามไปที่
- จากนั้นเรากลับ
ทีนี้ลองคิดดูว่าเกิดอะไรขึ้นนอกวิธีการ:
ไม่myDog
เปลี่ยนแปลง?
มีกุญแจสำคัญ
โปรดทราบว่าmyDog
เป็นตัวชี้ไม่ใช่ตัวชี้Dog
วัดคำตอบคือไม่ myDog
ยังคงมีค่า 42; มันยังคงชี้ไปที่ต้นฉบับDog
(แต่โปรดทราบว่าเนื่องจากบรรทัด "AAA" ตอนนี้ชื่อของมันคือ "Max" - ยังคงเป็น Dog เหมือนเดิมmyDog
ค่าของไม่เปลี่ยนแปลง)
สามารถติดตามที่อยู่ได้อย่างสมบูรณ์และเปลี่ยนแปลงสิ่งที่อยู่ในตอนท้าย ที่ไม่เปลี่ยนแปลงตัวแปรอย่างไรก็ตาม
Java ทำงานเหมือนกับ C. คุณสามารถกำหนดตัวชี้ส่งผ่านตัวชี้ไปยังวิธีการทำตามตัวชี้ในวิธีการและเปลี่ยนข้อมูลที่ชี้ไป อย่างไรก็ตามคุณไม่สามารถเปลี่ยนตำแหน่งที่ตัวชี้นั้นชี้ได้
ใน C ++, Ada, Pascal และภาษาอื่น ๆ ที่รองรับ pass-by-reference คุณสามารถเปลี่ยนตัวแปรที่ส่งผ่านไปได้
หาก Java มีความหมายแบบ pass-by-reference foo
วิธีที่เรากำหนดไว้ข้างต้นจะเปลี่ยนตำแหน่งที่myDog
ชี้เมื่อกำหนดsomeDog
บนบรรทัด BBB
ให้คิดว่าพารามิเตอร์อ้างอิงเป็นนามแฝงสำหรับตัวแปรที่ส่งผ่านเข้ามาเมื่อมีการกำหนดนามแฝงนั้นตัวแปรที่ส่งผ่านก็เช่นกัน
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");
}
}
ฉันจะอธิบายสิ่งนี้เป็นขั้นตอน:
ประกาศอ้างอิงชื่อ
f
ประเภทFoo
และกำหนดวัตถุใหม่ชนิดที่มีแอตทริบิวต์Foo
"f"
Foo f = new Foo("f");
จากด้านข้างวิธีการอ้างอิงจากประเภท
Foo
ที่มีชื่อถูกประกาศและจะได้รับมอบหมายแรกa
null
public static void changeReference(Foo a)
ในขณะที่คุณเรียกใช้เมธอด
changeReference
การอ้างอิงa
จะถูกกำหนดอ็อบเจ็กต์ซึ่งถูกส่งผ่านเป็นอาร์กิวเมนต์changeReference(f);
ประกาศอ้างอิงชื่อ
b
ประเภทFoo
และกำหนดวัตถุใหม่ชนิดที่มีแอตทริบิวต์Foo
"b"
Foo b = new Foo("b");
a = b
ทำให้งานใหม่ที่จะอ้างอิงa
, ไม่ได้ ของวัตถุที่มีแอตทริบิวต์f
"b"
ในขณะที่คุณเรียก
modifyReference(Foo c)
วิธีการอ้างอิงจะถูกสร้างขึ้นและได้รับมอบหมายวัตถุที่มีแอตทริบิวต์c
"f"
c.setAttribute("c");
จะเปลี่ยนแอตทริบิวต์ของวัตถุที่อ้างอิงc
ชี้ไปและเป็นวัตถุเดียวกันกับที่อ้างอิงf
ชี้ไป
ฉันหวังว่าคุณจะเข้าใจแล้วว่าการส่งผ่านวัตถุเป็นอาร์กิวเมนต์ทำงานอย่างไรใน Java :)
สิ่งนี้จะให้ข้อมูลเชิงลึกเกี่ยวกับวิธีการทำงานของ Java จนถึงจุดที่ในการสนทนาครั้งต่อไปของคุณเกี่ยวกับการส่งผ่าน Java โดยการอ้างอิงหรือการส่งผ่านค่าคุณจะยิ้มได้ :-)
ขั้นตอนที่หนึ่งโปรดลบออกจากใจของคุณคำที่ขึ้นต้นด้วย 'p' "_ _ _ _ _ _ _ _" โดยเฉพาะอย่างยิ่งถ้าคุณมาจากภาษาโปรแกรมอื่น Java และ 'p' ไม่สามารถเขียนในหนังสือฟอรัมหรือแม้แต่ txt เดียวกันได้
ขั้นตอนที่สองจำไว้ว่าเมื่อคุณส่ง Object ไปยังเมธอดคุณจะส่งผ่าน Object reference ไม่ใช่ Object นั้นเอง
- นักเรียน : อาจารย์หมายความว่า Java เป็น pass-by-reference หรือเปล่า?
- Master : Grasshopper, No.
ตอนนี้คิดว่าการอ้างอิง / ตัวแปรของวัตถุทำ / คืออะไร:
- ตัวแปรเก็บบิตที่บอก JVM ว่าจะไปยัง Object ที่อ้างอิงในหน่วยความจำ (Heap) ได้อย่างไร
- เมื่อผ่านการขัดแย้งที่จะเป็นวิธีการที่คุณยังไม่ได้ผ่านตัวแปรอ้างอิง แต่คัดลอกบิตในตัวแปรอ้างอิง สิ่งนี้: 3bad086a 3bad086a แสดงถึงวิธีการไปยังวัตถุที่ส่งผ่าน
- คุณก็แค่ส่ง 3bad086a นั่นก็คือค่าของข้อมูลอ้างอิง
- คุณกำลังส่งผ่านค่าของการอ้างอิงไม่ใช่การอ้างอิง (ไม่ใช่วัตถุ)
- ค่านี้จะถูกคัดลอกจริงและได้รับกับวิธีการ
ดังต่อไปนี้ (โปรดอย่าพยายามรวบรวม / ดำเนินการนี้ ... ):
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
- ตัวแปรบุคคลที่ถือแอดเดรสของ Object จะถูกส่งผ่านไปยังฟังก์ชันในบรรทัด # 3
- ในบรรทัด # 4 คุณสามารถฟังเสียงแห่งความเงียบได้
- ตรวจสอบความคิดเห็นในบรรทัด # 5
- ตัวแปรโลคัลเมธอด - anotherReferenceToTheSamePersonObject - ถูกสร้างขึ้นจากนั้นเวทมนตร์ในบรรทัด # 6:
- ตัวแปร / บุคคลอ้างอิงจะถูกคัดลอกทีละบิตและส่งต่อไปยังanotherReferenceToTheSamePersonObjectภายในฟังก์ชัน
- ไม่มีการสร้างอินสแตนซ์ใหม่ของบุคคล
- ทั้ง " person " และ " anotherReferenceToTheSamePersonObject " มีค่า 3bad086a เท่ากัน
- อย่าลอง แต่ person == anotherReferenceToTheSamePersonObject จะเป็นจริง
- ตัวแปรทั้งสองมีสำเนาประจำตัวของการอ้างอิงและทั้งสองอ้างถึงวัตถุบุคคลเดียวกันวัตถุเดียวกันบนกองและไม่ใช่สำเนา
ภาพที่มีค่าพันคำ:
โปรดสังเกตว่าลูกศร anotherReferenceToTheSamePersonObject มุ่งตรงไปยัง Object และไม่ใช่ไปที่ตัวแปร person!
หากคุณไม่ได้รับมันแล้วก็เชื่อใจฉันและจำไว้ว่ามันจะดีกว่าที่จะบอกว่าJava เป็นผ่านค่า ดีผ่านค่าอ้างอิง โอ้ยิ่งดีไปกว่านั้นคือpass-by-copy-of-the-variable-value! ;)
ตอนนี้อย่าลังเลที่จะเกลียดฉัน แต่โปรดทราบว่าเนื่องจากสิ่งนี้ไม่มีความแตกต่างระหว่างการส่งผ่านประเภทข้อมูลดั้งเดิมและ Objectsเมื่อพูดถึงข้อโต้แย้งของวิธีการ
คุณส่งสำเนาบิตของค่าอ้างอิงเสมอ!
- หากเป็นประเภทข้อมูลดั้งเดิมบิตเหล่านี้จะมีค่าของชนิดข้อมูลดั้งเดิมเอง
- ถ้าเป็น Object บิตจะมีค่าของแอดเดรสที่บอก JVM ว่าจะไปยัง Object ได้อย่างไร
Java เป็น pass-by-value เนื่องจากภายในเมธอดคุณสามารถแก้ไขอ็อบเจ็กต์ที่อ้างอิงได้มากเท่าที่คุณต้องการ แต่ไม่ว่าคุณจะพยายามแค่ไหนคุณก็จะไม่สามารถแก้ไขตัวแปรที่ส่งผ่านซึ่งจะทำการอ้างอิงต่อไป (ไม่ใช่ p _ _ _ _ _ _ _) วัตถุเดียวกันไม่ว่าจะเกิดอะไรขึ้น!
ฟังก์ชัน changeName ด้านบนจะไม่สามารถแก้ไขเนื้อหาจริง (ค่าบิต) ของการอ้างอิงที่ส่งผ่าน ในคำอื่น changeName ไม่สามารถทำให้บุคคลบุคคลอ้างถึงวัตถุอื่นได้
แน่นอนคุณสามารถตัดมันให้สั้นและพูดได้ว่าJava เป็น pass-by-value!
Java จะผ่านเสมอโดยค่าโดยไม่มีข้อยกเว้นที่เคย
แล้วใคร ๆ ก็สับสนกับเรื่องนี้ได้อย่างไรและเชื่อว่า Java ผ่านการอ้างอิงหรือคิดว่าพวกเขามีตัวอย่างของ Java ที่ทำหน้าที่เป็น pass by reference? ประเด็นสำคัญคือ Java ไม่เคยให้การเข้าถึงค่าของอ็อบเจ็กต์โดยตรงไม่ว่าในกรณีใด ๆ การเข้าถึงวัตถุเพียงอย่างเดียวคือผ่านการอ้างอิงไปยังวัตถุนั้น เพราะวัตถุ Java เป็นเสมอเข้าถึงได้ผ่านการอ้างอิงโดยตรงมากกว่ามันเป็นเรื่องธรรมดาที่จะพูดคุยเกี่ยวกับเขตข้อมูลและตัวแปรและข้อโต้แย้งวิธีการในฐานะที่เป็นวัตถุเมื่ออวดพวกเขาเป็นเพียงการอ้างอิงไปยังวัตถุ ความสับสนเกิดจากการเปลี่ยนแปลงในระบบการตั้งชื่อนี้ (พูดอย่างเคร่งครัดไม่ถูกต้อง)
ดังนั้นเมื่อเรียกใช้เมธอด
- สำหรับอาร์กิวเมนต์ดั้งเดิม (
int
,long
ฯลฯ ) ผ่านค่าคือค่าที่แท้จริงของดั้งเดิม (เช่น 3) - สำหรับวัตถุผ่านค่าคือค่าของการอ้างอิงไปยังวัตถุ
ดังนั้นหากคุณมีdoSomething(foo)
และpublic void doSomething(Foo foo) { .. }
Foos ทั้งสองได้คัดลอกการอ้างอิงที่ชี้ไปที่วัตถุเดียวกัน
โดยธรรมชาติแล้วการส่งผ่านค่าการอ้างอิงไปยังวัตถุนั้นดูคล้ายมาก (และแยกไม่ออกในทางปฏิบัติ) การส่งผ่านวัตถุโดยการอ้างอิง
Java ส่งผ่านการอ้างอิงตามค่า
คุณจึงไม่สามารถเปลี่ยนข้อมูลอ้างอิงที่ส่งผ่านได้
ฉันรู้สึกว่าการเถียงกันเกี่ยวกับ "pass-by-reference vs pass-by-value" นั้นไม่เป็นประโยชน์อย่างยิ่ง
หากคุณพูดว่า "Java คือ pass-by-any (การอ้างอิง / ค่า)" ไม่ว่าในกรณีใดคุณจะไม่ได้ให้คำตอบที่สมบูรณ์ นี่คือข้อมูลเพิ่มเติมที่หวังว่าจะช่วยในการทำความเข้าใจสิ่งที่เกิดขึ้นในความทรงจำ
หลักสูตรความผิดพลาดในสแต็ก / ฮีปก่อนที่เราจะนำไปใช้งาน Java: ค่าต่างๆจะเข้าและออกจากสแต็กอย่างเป็นระเบียบเรียบร้อยเช่นกองจานที่โรงอาหาร หน่วยความจำในฮีป (หรือที่เรียกว่าหน่วยความจำแบบไดนามิก) นั้นจับจดและไม่เป็นระเบียบ JVM เพียงแค่ค้นหาที่ว่างทุกที่ที่สามารถทำได้และทำให้เป็นอิสระเนื่องจากตัวแปรที่ใช้ไม่จำเป็นอีกต่อไป
ตกลง. ก่อนอื่นสิ่งดั้งเดิมในท้องถิ่นจะอยู่ในสแต็ก ดังนั้นรหัสนี้:
int x = 3;
float y = 101.1f;
boolean amIAwesome = true;
ผลลัพธ์ในสิ่งนี้:
เมื่อคุณประกาศและสร้างอินสแตนซ์วัตถุ วัตถุที่แท้จริงอยู่บนฮีป เกิดอะไรขึ้นในกอง? ที่อยู่ของวัตถุบนฮีป โปรแกรมเมอร์ C ++ จะเรียกสิ่งนี้ว่าพอยน์เตอร์ แต่นักพัฒนา Java บางคนไม่เห็นด้วยกับคำว่า "พอยน์เตอร์" ก็ตาม. เพียงแค่รู้ว่าที่อยู่ของวัตถุไปบนสแตก
ชอบมาก:
int problems = 99;
String name = "Jay-Z";
อาร์เรย์เป็นวัตถุดังนั้นมันจึงไปอยู่บนฮีปเช่นกัน แล้ววัตถุในอาร์เรย์ล่ะ? พวกเขาได้รับพื้นที่ฮีปของตัวเองและที่อยู่ของแต่ละวัตถุจะอยู่ภายในอาร์เรย์
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);
}
One String ถูกสร้างขึ้นและมีการจัดสรรพื้นที่ในฮีปและแอดเดรสของสตริงจะถูกเก็บไว้ในสแต็กและกำหนดตัวระบุhisName
เนื่องจากแอดเดรสของสตริงที่สองจะเหมือนกับสตริงแรกจึงไม่มีการสร้างสตริงใหม่และ ไม่มีการจัดสรรพื้นที่ฮีปใหม่ แต่มีการสร้างตัวระบุใหม่บนสแต็ก จากนั้นเราเรียกว่าshout()
: สร้างสแต็กเฟรมใหม่และตัวระบุใหม่name
ถูกสร้างและกำหนดที่อยู่ของสตริงที่มีอยู่แล้ว
ดังนั้นค่าอ้างอิง? คุณพูดว่า "มันฝรั่ง"
เพียงเพื่อแสดงความคมชัดให้เปรียบเทียบตัวอย่าง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 มีการส่งผ่านสองประเภทเท่านั้น: ตามค่าสำหรับชนิดในตัวและตามค่าของตัวชี้สำหรับชนิดอ็อบเจ็กต์
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
จะมีนิยามใหม่ให้กับbaz
null
ฉันไม่อยากจะเชื่อเลยว่ายังไม่มีใครพูดถึง Barbara Liskov เมื่อเธอออกแบบ CLU ในปีพ. ศ. 2517 เธอประสบปัญหาคำศัพท์เดียวกันนี้และเธอได้คิดค้นคำเรียกโดยการแชร์ (หรือที่เรียกว่าการเรียกโดยการแชร์วัตถุและการเรียกตามวัตถุ ) สำหรับกรณีเฉพาะนี้ของ "การเรียกตามค่าโดยที่ค่าเป็น เอกสารอ้างอิง ".
ประเด็นสำคัญคือการอ้างอิงคำในนิพจน์ "pass by reference" หมายถึงสิ่งที่แตกต่างอย่างสิ้นเชิงจากความหมายปกติของการอ้างอิงคำใน Java
มักจะอยู่ใน Java อ้างอิงหมายความ AA อ้างอิงถึงวัตถุ แต่คำศัพท์ทางเทคนิคผ่านการอ้างอิง / ค่าจากทฤษฎีภาษาการเขียนโปรแกรมกำลังพูดถึงการอ้างอิงถึงเซลล์หน่วยความจำที่ถือตัวแปรซึ่งเป็นสิ่งที่แตกต่างอย่างสิ้นเชิง
ใน java ทุกอย่างอ้างอิงดังนั้นเมื่อคุณมีสิ่งที่ต้องการ: Point pnt1 = new Point(0,0);
Java ทำตาม:
- สร้างวัตถุจุดใหม่
- สร้างการอ้างอิงจุดใหม่และเริ่มต้นการอ้างอิงนั้นไปยังจุด (อ้างถึง)บนวัตถุจุดที่สร้างไว้ก่อนหน้านี้
- จากที่นี่ผ่าน Point object life คุณจะเข้าถึงออบเจ็กต์นั้นผ่านการอ้างอิง 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);
การสร้างวัตถุสองจุดที่แตกต่างกันโดยมีการอ้างอิงที่แตกต่างกันสองรายการที่เกี่ยวข้อง
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
ดังนั้น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
, , แต่ข้อสังเกตที่สำคัญคือทุกสิ่งที่คุณทำกับการอ้างอิงเหล่านี้เมื่อมี 'ในชีวิต' จะส่งผลอย่างถาวรต่อวัตถุที่พวกเขาชี้ไปarg1
arg2
temp
ดังนั้นหลังจากเรียกใช้เมธอดtricky
เมื่อคุณกลับไปmain
คุณจะมีสถานการณ์นี้:
ตอนนี้การเรียกใช้โปรแกรมอย่างสมบูรณ์จะเป็น:
X1: 0 Y1: 0
X2: 0 Y2: 0
X1: 100 Y1: 100
X2: 0 Y2: 0
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
ดังที่แสดงด้านล่าง:
หวังว่านี่จะช่วยได้
มีคำตอบที่ดีอยู่แล้วที่ครอบคลุมเรื่องนี้ ฉันต้องการมีส่วนร่วมเล็กน้อยโดยการแบ่งปันตัวอย่างง่ายๆ (ซึ่งจะรวบรวม) ที่ตัดกันพฤติกรรมระหว่าง Pass-by-reference ใน c ++ และ Pass-by-value ใน Java
บางจุด:
- คำว่า "การอ้างอิง" เป็นคำที่มีสองความหมายแยกกันมากเกินไป ใน Java หมายถึงตัวชี้ แต่ในบริบทของ "Pass-by-reference" หมายถึงตัวจัดการตัวแปรดั้งเดิมที่ส่งผ่านเข้ามา
- Java เป็นผ่านโดยค่า Java สืบเชื้อสายมาจาก C (ในภาษาอื่น ๆ ) ก่อนหน้า C ภาษาก่อนหน้านี้หลายภาษา (แต่ไม่ใช่ทั้งหมด) เช่น FORTRAN และ COBOL รองรับ PBR แต่ C ไม่รองรับ PBR อนุญาตให้ภาษาอื่น ๆ เหล่านี้ทำการเปลี่ยนแปลงตัวแปรที่ส่งผ่านภายในรูทีนย่อย เพื่อให้บรรลุสิ่งเดียวกัน (เช่นเปลี่ยนค่าของตัวแปรภายในฟังก์ชัน) โปรแกรมเมอร์ C ส่งพอยน์เตอร์ไปยังตัวแปรเป็นฟังก์ชัน ภาษาที่ได้รับแรงบันดาลใจจาก C เช่น Java ยืมแนวคิดนี้และส่งต่อตัวชี้ไปยังวิธีการเช่นเดียวกับ C ยกเว้น Java เรียกตัวชี้การอ้างอิง อีกครั้งนี่เป็นการใช้คำว่า "Reference" ที่แตกต่างจากใน "Pass-By-Reference"
- C ++ ช่วยให้ Pass-by-referenceโดยการประกาศพารามิเตอร์อ้างอิงโดยใช้อักขระ "&" (ซึ่งเป็นอักขระเดียวกับที่ใช้ระบุ "ที่อยู่ของตัวแปร" ทั้งใน C และ C ++) ตัวอย่างเช่นหากเราส่งผ่านตัวชี้โดยการอ้างอิงพารามิเตอร์และอาร์กิวเมนต์ไม่ได้ชี้ไปที่วัตถุเดียวกันเท่านั้น แต่เป็นตัวแปรเดียวกัน หากได้รับการตั้งค่าเป็นที่อยู่อื่นหรือเป็นโมฆะอีกอันก็เช่นกัน
- ใน 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 pass "a Java reference" ตามตัวอย่างค่า
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 เป็น nil (นั่นคือ pascal พูดแทน 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 และความแตกต่างระหว่าง pass-by-reference และ pass-by-value และเป็นของผู้สร้าง Java ฉันขอแนะนำให้ทุกคนอ่านโดยเฉพาะอย่างยิ่งหากคุณยังไม่มั่นใจ
ฉันคิดว่าความแตกต่างระหว่างสองรุ่นนั้นมีความละเอียดอ่อนมากและถ้าคุณไม่ได้เขียนโปรแกรมที่คุณใช้ pass-by-reference จริงๆแล้วมันก็ง่ายที่จะพลาดตรงที่ทั้งสองรุ่นต่างกัน
ฉันหวังว่าสิ่งนี้จะยุติการอภิปราย แต่อาจจะไม่เป็นเช่นนั้น
แก้ไข 3
ฉันอาจจะหมกมุ่นอยู่กับกระทู้นี้เล็กน้อย อาจเป็นเพราะฉันรู้สึกว่าผู้สร้าง Java เผยแพร่ข้อมูลที่ผิดโดยไม่ได้ตั้งใจ ถ้าแทนที่จะใช้คำว่า "reference" สำหรับคำชี้ที่พวกเขาใช้อย่างอื่นให้พูดว่า dingleberry ก็ไม่มีปัญหา คุณสามารถพูดได้ว่า "Java ส่งผ่าน dingleberries ด้วยมูลค่าไม่ใช่โดยการอ้างอิง" ก็ไม่มีใครสับสน
นั่นเป็นเหตุผลที่มีเพียงนักพัฒนา Java เท่านั้นที่มีปัญหากับสิ่งนี้ พวกเขามองไปที่คำว่า "การอ้างอิง" และคิดว่าพวกเขารู้ดีว่ามันหมายถึงอะไรดังนั้นพวกเขาจึงไม่ต้องกังวลที่จะพิจารณาข้อโต้แย้งของฝ่ายตรงข้าม
อย่างไรก็ตามฉันสังเกตเห็นความคิดเห็นในโพสต์ที่เก่ากว่าซึ่งทำให้คล้ายคลึงกับบอลลูนที่ฉันชอบมาก มากจนฉันตัดสินใจที่จะรวมคลิปอาร์ตเข้าด้วยกันเพื่อสร้างชุดการ์ตูนเพื่อแสดงให้เห็นประเด็น
การส่งผ่านการอ้างอิงตามค่า - การเปลี่ยนแปลงการอ้างอิงจะไม่แสดงในขอบเขตของผู้เรียก แต่การเปลี่ยนแปลงของออบเจ็กต์คือ เนื่องจากการอ้างอิงถูกคัดลอก แต่ทั้งต้นฉบับและสำเนาอ้างถึงวัตถุเดียวกัน
ผ่านการอ้างอิง -ไม่มีสำเนาเอกสารอ้างอิง การอ้างอิงเดี่ยวจะใช้ร่วมกันโดยทั้งผู้เรียกและฟังก์ชันที่ถูกเรียก การเปลี่ยนแปลงการอ้างอิงหรือข้อมูลของวัตถุจะแสดงในขอบเขตของผู้โทร
แก้ไข 4
ฉันได้เห็นโพสต์ในหัวข้อนี้ซึ่งอธิบายถึงการใช้พารามิเตอร์ในระดับต่ำใน Java ซึ่งฉันคิดว่าดีและมีประโยชน์มากเพราะมันทำให้แนวคิดนามธรรมเป็นรูปธรรม อย่างไรก็ตามสำหรับฉันคำถามเกี่ยวกับพฤติกรรมที่อธิบายไว้ในข้อกำหนดภาษามากกว่าเกี่ยวกับการใช้งานทางเทคนิคของพฤติกรรม นี่เป็นการคัดลอกมาจากข้อกำหนดภาษา Java ส่วน 8.4.1 :
เมื่อเรียกใช้เมธอดหรือคอนสตรัคเตอร์ (§15.12) ค่าของนิพจน์อาร์กิวเมนต์ที่แท้จริงจะเริ่มต้นตัวแปรพารามิเตอร์ที่สร้างขึ้นใหม่แต่ละประเภทที่ประกาศก่อนที่จะดำเนินการเนื้อหาของเมธอดหรือตัวสร้าง ตัวระบุที่ปรากฏใน DeclaratorId อาจใช้เป็นชื่อธรรมดาในเนื้อความของวิธีการหรือตัวสร้างเพื่ออ้างถึงพารามิเตอร์ที่เป็นทางการ
ซึ่งหมายความว่า java จะสร้างสำเนาของพารามิเตอร์ที่ผ่านก่อนที่จะเรียกใช้เมธอด เช่นเดียวกับคนส่วนใหญ่ที่เรียนคอมไพเลอร์ในวิทยาลัยฉันใช้"The Dragon Book"ซึ่งเป็นหนังสือTHE compilers มีคำอธิบายที่ดีของ "Call-by-value" และ "Call-by-Reference" ในบทที่ 1 คำอธิบาย Call-by-value ตรงกับ Java Specs ทุกประการ
ย้อนกลับไปตอนที่ฉันเรียนคอมไพเลอร์ในช่วงทศวรรษที่ 90 ฉันใช้หนังสือรุ่นแรกจากปี 1986 ซึ่งก่อนวันที่ Java ประมาณ 9 หรือ 10 ปี อย่างไรก็ตามฉันเพิ่งพบสำเนาของการแก้ไขครั้งที่ 2จากปี 2550 ซึ่งกล่าวถึง Java! ส่วนที่ 1.6.6 ระบุว่า "กลไกการส่งผ่านพารามิเตอร์" อธิบายถึงพารามิเตอร์ที่ส่งผ่านได้อย่างสวยงาม นี่คือข้อความที่ตัดตอนมาภายใต้หัวข้อ "Call-by-value" ซึ่งกล่าวถึง Java:
ในการเรียกตามค่าพารามิเตอร์จริงจะถูกประเมิน (ถ้าเป็นนิพจน์) หรือคัดลอก (ถ้าเป็นตัวแปร) ค่าถูกวางไว้ในตำแหน่งที่เป็นของพารามิเตอร์ที่เป็นทางการที่เกี่ยวข้องของโพรซีเดอร์ที่เรียก วิธีนี้ใช้ใน C และ Java และเป็นตัวเลือกทั่วไปใน C ++ เช่นเดียวกับภาษาอื่น ๆ ส่วนใหญ่
Java เป็นการเรียกตามค่า
มันทำงานอย่างไร
คุณส่งสำเนาบิตของค่าอ้างอิงเสมอ!
หากเป็นประเภทข้อมูลดั้งเดิมบิตเหล่านี้มีค่าของชนิดข้อมูลดั้งเดิมด้วยเหตุนี้หากเราเปลี่ยนค่าของส่วนหัวภายในวิธีการนั้นจะไม่สะท้อนการเปลี่ยนแปลงภายนอก
หากเป็นประเภทข้อมูลออบเจ็กต์เช่นFoo foo = new Foo ()จากนั้นในกรณีนี้สำเนาของที่อยู่ของวัตถุจะผ่านไปเช่นทางลัดไฟล์สมมติว่าเรามีไฟล์ข้อความabc.txtที่C: \ desktopและสมมติว่าเราสร้างทางลัดของ ไฟล์เดียวกันและวางไว้ในC: \ desktop \ abc-shortcutดังนั้นเมื่อคุณเข้าถึงไฟล์จากC: \ desktop \ abc.txtและเขียน'Stack Overflow'และปิดไฟล์จากนั้นคุณจะเปิดไฟล์อีกครั้งจากทางลัด เขียน'เป็นชุมชนออนไลน์ที่ใหญ่ที่สุดสำหรับโปรแกรมเมอร์ในการเรียนรู้'จากนั้นการเปลี่ยนแปลงไฟล์ทั้งหมดจะเป็น'Stack Overflow เป็นชุมชนออนไลน์ที่ใหญ่ที่สุดสำหรับโปรแกรมเมอร์ในการเรียนรู้'ซึ่งหมายความว่าไม่สำคัญว่าคุณจะเปิดไฟล์จากที่ใดในแต่ละครั้งที่เราเข้าถึง ไฟล์เดียวกันที่นี่เราสามารถสมมติFooเป็นไฟล์และสมมติว่า foo เก็บไว้ที่123hd7h (ที่อยู่เดิมเช่นที่อยู่C: \ desktop \ abc.txt ) และ234jdid (ที่อยู่ที่คัดลอกเช่นC: \ desktop \ abc-shortcutซึ่งจริงๆแล้วมี ที่อยู่เดิมของไฟล์ภายใน) .. ดังนั้นเพื่อความเข้าใจที่ดีขึ้นให้สร้าง Shortcut file และรู้สึก ..
การอ้างอิงเป็นค่าเสมอเมื่อแสดงไม่ว่าคุณจะใช้ภาษาอะไร
รับมุมมองนอกกรอบมาดูที่ Assembly หรือการจัดการหน่วยความจำระดับต่ำ ในระดับ 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
นี่คือวิธีการทำงานของตารางกระโดด
หากเราต้องการเรียกใช้ method / function / procedure ด้วยค่าของ Foo มีสองสามวิธีที่เป็นไปได้ในการส่งผ่านตัวแปรไปยังเมธอดทั้งนี้ขึ้นอยู่กับภาษาและโหมดการเรียกใช้เมธอดต่างๆ:
- 5 ถูกคัดลอกไปยังหนึ่งในการลงทะเบียน CPU (เช่น EAX)
- 5 ได้รับ PUSHd ไปยังสแต็ก
- 47 ถูกคัดลอกไปยังหนึ่งในการลงทะเบียน CPU
- 47 PUSHd ไปที่สแต็ก
- 223 ถูกคัดลอกไปยังหนึ่งในการลงทะเบียน CPU
- 223 ได้รับ PUSHd ไปยังสแต็ก
ในทุกกรณีที่อยู่เหนือค่า - มีการสร้างสำเนาของค่าที่มีอยู่แล้วตอนนี้ก็เป็นวิธีการรับที่จะจัดการกับมัน เมื่อคุณเขียน "Foo" ภายในเมธอดมันจะถูกอ่านจาก EAX หรือยกเลิกการอ้างอิงโดยอัตโนมัติหรือการอ้างอิงสองครั้งกระบวนการจะขึ้นอยู่กับวิธีการทำงานของภาษาและ / หรือประเภทของ Foo ที่กำหนด สิ่งนี้ถูกซ่อนจากนักพัฒนาจนกว่าเธอจะหลีกเลี่ยงขั้นตอนการอ้างอิง ดังนั้นการอ้างอิงจึงเป็นค่าเมื่อแสดงเนื่องจากการอ้างอิงเป็นค่าที่ต้องดำเนินการ (ในระดับภาษา)
ตอนนี้เราได้ส่ง Foo ไปยังวิธีการ:
- ในกรณีที่ 1. และ 2. หากคุณเปลี่ยน Foo (
Foo = 9
) จะมีผลเฉพาะขอบเขตเฉพาะที่คุณมีสำเนาของค่า จากภายในวิธีการเราไม่สามารถระบุได้ว่า Foo ดั้งเดิมอยู่ที่ใดในหน่วยความจำ - ในกรณีที่ 3. และ 4. หากคุณใช้โครงสร้างภาษาเริ่มต้นและเปลี่ยน Foo (
Foo = 11
) มันสามารถเปลี่ยน Foo ได้ทั่วโลก (ขึ้นอยู่กับภาษาเช่น Java หรือเหมือนกับprocedure findMin(x, y, z: integer;
var mของ Pascal: integer);
) แต่ถ้าภาษาที่ช่วยให้คุณสามารถที่จะหลีกเลี่ยงกระบวนการ dereference คุณสามารถเปลี่ยนพูดกับ47
49
เมื่อถึงจุดนั้น Foo ดูเหมือนจะเปลี่ยนไปถ้าคุณอ่านเพราะคุณเปลี่ยนตัวชี้ท้องถิ่นเป็นมัน และถ้าคุณแก้ไข Foo นี้ภายในเมธอด (Foo = 12
) คุณอาจจะ FUBAR การดำเนินการของโปรแกรม (aka. segfault) เนื่องจากคุณจะเขียนไปยังหน่วยความจำที่แตกต่างจากที่คาดไว้คุณยังสามารถปรับเปลี่ยนพื้นที่ที่กำหนดให้สามารถปฏิบัติการได้ โปรแกรมและการเขียนมันจะแก้ไขโค้ดที่กำลังทำงานอยู่ (ตอนนี้ Foo ไม่อยู่ที่47
) แต่ค่าของ Foo47
ไม่ได้เปลี่ยนไปทั่วโลกมีเพียงค่าเดียวที่อยู่ในเมธอดเท่านั้นเนื่องจาก47
เป็นสำเนาของเมธอดด้วย - ในกรณีที่ 5. และ 6. หากคุณแก้ไข
223
ภายในเมธอดมันจะสร้างการทำร้ายร่างกายเช่นเดียวกับในข้อ 3. หรือ 4 (ตัวชี้ที่ชี้ไปยังค่าที่ไม่ดีในขณะนี้ซึ่งถูกใช้เป็นตัวชี้อีกครั้ง) แต่ยังคงเป็นแบบโลคัล ปัญหาเป็น 223 ถูกคัดลอก อย่างไรก็ตามหากคุณสามารถหักล้างการอ้างอิงRef2Foo
(นั่นคือ223
) เข้าถึงและแก้ไขค่าที่ชี้ได้47
กล่าวว่าถึง49
จะส่งผลต่อ Foo ทั่วโลกเพราะในกรณีนี้เมธอดมีสำเนา223
แต่มีการอ้างอิง47
เพียงครั้งเดียวและเปลี่ยนสิ่งนั้น to49
จะทำให้ทุกRef2Foo
การอ้างอิงสองครั้งมีค่าที่ไม่ถูกต้อง
การมองข้ามรายละเอียดที่ไม่สำคัญแม้แต่ภาษาที่ผ่านการอ้างอิงจะส่งผ่านค่าไปยังฟังก์ชัน แต่ฟังก์ชันเหล่านั้นทราบดีว่าต้องใช้เพื่อวัตถุประสงค์ในการอ้างอิง นี้ผ่านที่อ้างอิงเป็นค่าที่ถูกซ่อนอยู่เพียงโปรแกรมเมอร์เพราะมันจะไม่ได้ผลในทางปฏิบัติและคำศัพท์เป็นเพียงการส่งผ่านโดยการอ้างอิง
pass-by-value ที่เข้มงวดก็ไม่มีประโยชน์เช่นกันหมายความว่าต้องคัดลอกอาร์เรย์ 100 Mbyte ทุกครั้งที่เราเรียกเมธอดที่มีอาร์เรย์ว่าอาร์กิวเมนต์ดังนั้น Java จึงไม่สามารถเป็น pass-by-value ที่เข้มงวดได้ ทุกภาษาจะส่งการอ้างอิงไปยังอาร์เรย์ขนาดใหญ่นี้ (เป็นค่า) และใช้กลไกการคัดลอกเมื่อเขียนหากอาร์เรย์นั้นสามารถเปลี่ยนแปลงได้ภายในวิธีการหรืออนุญาตให้เมธอด (เช่นเดียวกับ 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 เนื่องจากในฟังก์ชัน swap คุณใช้ 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);
}
อย่างไรก็ตามคุณสามารถสร้าง 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 เมื่อพูดถึงการ "เพิ่ม" สองสตริงเพราะคุณสามารถแก้ไขวัตถุดั้งเดิมที่คุณไม่สามารถใช้กับวัตถุที่ไม่เปลี่ยนรูปได้เช่น String is
ไม่มันไม่ผ่านการอ้างอิง
Java ส่งผ่านค่าตามข้อกำหนดภาษา Java:
เมื่อเรียกใช้เมธอดหรือคอนสตรัคเตอร์ (§15.12) ค่าของนิพจน์อาร์กิวเมนต์ที่แท้จริงจะเริ่มต้นตัวแปรพารามิเตอร์ที่สร้างขึ้นใหม่แต่ละประเภทที่ประกาศก่อนที่จะดำเนินการเนื้อหาของเมธอดหรือตัวสร้าง ตัวบ่งชี้ที่ปรากฏใน DeclaratorId อาจจะใช้เป็นชื่อที่ง่ายในร่างกายของวิธีการหรือสร้างเพื่ออ้างถึงพารามิเตอร์อย่างเป็นทางการ
ให้ฉันพยายามอธิบายความเข้าใจของฉันด้วยความช่วยเหลือจากสี่ตัวอย่าง Java เป็น pass-by-value และไม่ใช่ pass-by-reference
/ **
ส่งผ่านค่า
ใน 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 '
บางคนบอกว่าประเภทดั้งเดิมและ 'String' คือ 'pass by value' และ object คือ 'pass by reference'
แต่จากตัวอย่างนี้เราสามารถเข้าใจได้ว่า infact pass by value เท่านั้นโปรดทราบว่าที่นี่เรากำลังส่งข้อมูลอ้างอิงเป็นค่า กล่าวคือ: การอ้างอิงถูกส่งผ่านด้วยค่า นั่นเป็นเหตุผลที่สามารถเปลี่ยนแปลงได้และยังคงเป็นจริงหลังจากขอบเขตท้องถิ่น แต่เราไม่สามารถเปลี่ยนการอ้างอิงจริงนอกขอบเขตเดิมได้ ความหมายดังกล่าวแสดงให้เห็นโดยตัวอย่างถัดไปของ 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 ได้และวิธีหนึ่งที่เห็นได้ชัดคือเมื่อคุณต้องการส่งคืนค่ามากกว่าหนึ่งค่าจากการเรียกใช้เมธอด พิจารณาบิตของโค้ดต่อไปนี้ใน 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
ฉันคิดว่าฉันจะให้คำตอบนี้เพื่อเพิ่มรายละเอียดเพิ่มเติมจากข้อมูลจำเพาะ
อันดับแรกอะไรคือความแตกต่างระหว่างการส่งผ่านข้อมูลอ้างอิงกับการส่งผ่านค่า?
การส่งผ่านการอ้างอิงหมายถึงพารามิเตอร์ของฟังก์ชันที่เรียกว่าจะเหมือนกับอาร์กิวเมนต์ที่ส่งผ่านของผู้โทร (ไม่ใช่ค่า แต่เป็นตัวระบุ - ตัวแปรเอง)
การส่งผ่านค่าหมายถึงพารามิเตอร์ของฟังก์ชันที่เรียกว่าจะเป็นสำเนาของอาร์กิวเมนต์ที่ส่งผ่านของผู้โทร
หรือจากวิกิพีเดียเรื่อง pass-by-reference
ในการประเมินการเรียกโดยการอ้างอิง (เรียกอีกอย่างว่า pass-by-reference) ฟังก์ชันจะได้รับการอ้างอิงโดยปริยายไปยังตัวแปรที่ใช้เป็นอาร์กิวเมนต์แทนที่จะเป็นสำเนาของค่า โดยทั่วไปหมายความว่าฟังก์ชันสามารถแก้ไข (เช่นกำหนดให้) ตัวแปรที่ใช้เป็นอาร์กิวเมนต์ - สิ่งที่ผู้เรียกจะเห็น
และในเรื่องของ pass-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
นี่คือสิ่งที่อธิบายคำจำกัดความของ pass-by-value เช่นJava เป็นผ่านโดยค่า
ความจริงที่ว่าคุณสามารถทำตามการอ้างอิงเพื่อเรียกใช้เมธอดหรือเข้าถึงฟิลด์ของวัตถุที่อ้างอิงนั้นไม่เกี่ยวข้องกับการสนทนาโดยสิ้นเชิง คำจำกัดความของ pass-by-reference คือ
โดยทั่วไปหมายความว่าฟังก์ชันสามารถแก้ไข (เช่นกำหนดให้) ตัวแปรที่ใช้เป็นอาร์กิวเมนต์ - สิ่งที่ผู้เรียกจะเห็น
ใน Java การแก้ไขตัวแปรหมายถึงการกำหนดตัวแปรใหม่ ใน Java หากคุณกำหนดตัวแปรใหม่ภายในเมธอดมันจะไม่มีใครสังเกตเห็นผู้เรียกใช้ การแก้ไขวัตถุที่อ้างอิงโดยตัวแปรเป็นแนวคิดที่แตกต่างกันโดยสิ้นเชิง
ค่าดั้งเดิมมีการกำหนดไว้ยังอยู่ใน Java เครื่องเสมือนข้อมูลจำเพาะที่นี่ ค่าของประเภทคือค่าอินทิกรัลหรือทศนิยมที่สอดคล้องกันซึ่งเข้ารหัสอย่างเหมาะสม (บิต 8, 16, 32, 64 ฯลฯ )
ใน Java จะส่งผ่านการอ้างอิงเท่านั้นและถูกส่งผ่านด้วยค่า:
อาร์กิวเมนต์ Java ถูกส่งผ่านด้วยค่าทั้งหมด (การอ้างอิงจะถูกคัดลอกเมื่อใช้โดยวิธีการ):
ในกรณีของชนิดดั้งเดิมพฤติกรรมของ Java นั้นเรียบง่าย: ค่าจะถูกคัดลอกในอินสแตนซ์อื่นของชนิดดั้งเดิม
ในกรณีของ Object ตัวแปรนี้จะเหมือนกัน: ตัวแปร Object คือพอยน์เตอร์ (ที่เก็บข้อมูล) ซึ่งถือเฉพาะแอดเดรสของ Object ที่สร้างขึ้นโดยใช้คีย์เวิร์ด "new" และจะถูกคัดลอกเหมือนประเภทดั้งเดิม
ลักษณะการทำงานอาจแตกต่างจากประเภทดั้งเดิม: เนื่องจากตัวแปรอ็อบเจ็กต์ที่คัดลอกมีแอดเดรสเดียวกัน (ไปยังอ็อบเจ็กต์เดียวกัน) เนื้อหา / สมาชิกของออบเจ็กต์อาจยังคงได้รับการแก้ไขภายในวิธีการและการเข้าถึงภายนอกในภายหลังทำให้เกิดภาพลวงตาว่า (มี) อ็อบเจ็กต์นั้นถูกส่งผ่านโดยการอ้างอิง
วัตถุ "สตริง" ดูเหมือนจะเป็นตัวอย่างที่ดีสำหรับตำนานเมืองที่บอกว่า "วัตถุถูกส่งต่อโดยการอ้างอิง":
โดยใช้วิธีการคุณจะไม่สามารถอัปเดตค่าของสตริงที่ส่งผ่านเป็นอาร์กิวเมนต์ได้:
String Object เก็บอักขระโดยอาร์เรย์ที่ประกาศขั้นสุดท้ายซึ่งไม่สามารถแก้ไขได้ เฉพาะที่อยู่ของวัตถุเท่านั้นที่อาจถูกแทนที่โดยใช้ "ใหม่" การใช้ "new" เพื่ออัปเดตตัวแปรจะไม่อนุญาตให้เข้าถึง Object จากภายนอกเนื่องจากตัวแปรถูกส่งผ่านค่าและคัดลอกในตอนแรก
ความแตกต่างหรืออาจเป็นเพียงวิธีที่ฉันจำได้ในขณะที่ฉันเคยอยู่ภายใต้ความประทับใจเดียวกันกับโปสเตอร์ต้นฉบับคือ Java มักจะส่งผ่านค่า อ็อบเจ็กต์ทั้งหมด (ใน Java, ทุกอย่างยกเว้นสำหรับ primitives) ใน Java เป็นการอ้างอิง การอ้างอิงเหล่านี้จะส่งผ่านค่า
ดังที่หลาย ๆ คนเคยกล่าวไว้ก่อนหน้านี้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 จะเปลี่ยนเฉพาะในขอบเขตเฉพาะที่และไม่อยู่นอกวิธีนี้
แตกต่างจากภาษาอื่น ๆ Java ไม่อนุญาตให้คุณเลือกระหว่าง pass-by-value และ pass-by-reference - อาร์กิวเมนต์ทั้งหมดจะส่งผ่านค่า การเรียกเมธอดสามารถส่งผ่านค่าสองประเภทไปยังเมธอด - สำเนาของค่าดั้งเดิม (เช่นค่าของ int และ double) และสำเนาการอ้างอิงไปยังอ็อบเจ็กต์
เมื่อเมธอดแก้ไขพารามิเตอร์ชนิดดั้งเดิมการเปลี่ยนแปลงพารามิเตอร์จะไม่มีผลกับค่าอาร์กิวเมนต์ดั้งเดิมในวิธีการเรียก
เมื่อพูดถึงวัตถุวัตถุเองจะไม่สามารถส่งผ่านไปยังวิธีการได้ ดังนั้นเราจึงส่งข้อมูลอ้างอิง (ที่อยู่) ของวัตถุ เราสามารถปรับแต่งวัตถุดั้งเดิมโดยใช้การอ้างอิงนี้
Java สร้างและจัดเก็บวัตถุอย่างไร:เมื่อเราสร้างวัตถุเราจะจัดเก็บที่อยู่ของวัตถุไว้ในตัวแปรอ้างอิง ลองวิเคราะห์ข้อความต่อไปนี้
Account account1 = new Account();
“ บัญชี account1” คือประเภทและชื่อของตัวแปรอ้างอิง“ =” คือตัวดำเนินการกำหนด“ ใหม่” จะขอจำนวนพื้นที่ที่ต้องการจากระบบ ตัวสร้างทางด้านขวาของคีย์เวิร์ดใหม่ซึ่งสร้างอ็อบเจกต์เรียกโดยปริยายโดยคีย์เวิร์ดใหม่ ที่อยู่ของออบเจ็กต์ที่สร้างขึ้น (ผลลัพธ์ของค่าที่ถูกต้องซึ่งเป็นนิพจน์ที่เรียกว่า "นิพจน์การสร้างอินสแตนซ์คลาส") ถูกกำหนดให้กับค่าทางซ้าย (ซึ่งเป็นตัวแปรอ้างอิงที่มีชื่อและประเภทที่ระบุ) โดยใช้ตัวดำเนินการกำหนด
แม้ว่าการอ้างอิงของอ็อบเจ็กต์จะถูกส่งผ่านด้วยค่า แต่เมธอดยังคงสามารถโต้ตอบกับอ็อบเจ็กต์ที่อ้างอิงได้โดยเรียกใช้เมธอดสาธารณะโดยใช้สำเนาการอ้างอิงของอ็อบเจ็กต์ เนื่องจากการอ้างอิงที่เก็บไว้ในพารามิเตอร์คือสำเนาของการอ้างอิงที่ส่งผ่านเป็นอาร์กิวเมนต์พารามิเตอร์ในเมธอดที่เรียกและอาร์กิวเมนต์ในวิธีการเรียกจะอ้างถึงวัตถุเดียวกันในหน่วยความจำ
การส่งผ่านการอ้างอิงไปยังอาร์เรย์แทนที่จะเป็นอ็อบเจ็กต์อาร์เรย์ด้วยตัวเองนั้นเหมาะสมสำหรับเหตุผลด้านประสิทธิภาพ เนื่องจากทุกอย่างใน Java ถูกส่งผ่านด้วยค่าหากวัตถุอาร์เรย์ถูกส่งผ่านสำเนาของแต่ละองค์ประกอบจะถูกส่งผ่าน สำหรับอาร์เรย์ขนาดใหญ่สิ่งนี้จะเสียเวลาและใช้พื้นที่จัดเก็บข้อมูลจำนวนมากสำหรับสำเนาขององค์ประกอบ
ในภาพด้านล่างคุณจะเห็นว่าเรามีตัวแปรอ้างอิงสองตัว (สิ่งเหล่านี้เรียกว่าพอยน์เตอร์ใน C / C ++ และฉันคิดว่าคำนั้นทำให้เข้าใจคุณสมบัตินี้ได้ง่ายขึ้น) ในวิธีการหลัก ตัวแปรดั้งเดิมและตัวแปรอ้างอิงจะถูกเก็บไว้ในหน่วยความจำสแต็ก (ด้านซ้ายในภาพด้านล่าง) ตัวแปรอ้างอิง array1 และ array2 "point" (ตามที่โปรแกรมเมอร์ C / C ++ เรียกมัน) หรืออ้างอิงไปยังอาร์เรย์ a และ b ตามลำดับซึ่งเป็นวัตถุ (ค่าตัวแปรอ้างอิงเหล่านี้ถือเป็นที่อยู่ของวัตถุ) ในหน่วยความจำฮีป (ด้านขวาในภาพด้านล่าง) .
ถ้าเราส่งค่าของตัวแปรอ้างอิง 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);
}
}
ดังนั้นถ้าเราพูด
array1[0] = 5;
ในเมธอด reverseArray มันจะทำการเปลี่ยนแปลงในอาร์เรย์ a.
เรามีตัวแปรอ้างอิงอีกตัวในเมธอด reverseArray (array2) ที่ชี้ไปยังอาร์เรย์ c ถ้าเราจะพูด
array1 = array2;
ในเมธอด reverseArray จากนั้นตัวแปรอ้างอิง array1 ในเมธอด reverseArray จะหยุดชี้ไปที่อาร์เรย์ a และเริ่มชี้ไปที่อาร์เรย์ c (เส้นประในภาพที่สอง)
ถ้าเราคืนค่าของตัวแปรอ้างอิง array2 เป็นค่าส่งกลับของเมธอด 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) จะหายไป ซึ่งหมายความว่าตอนนี้เรามีเพียงสองตัวแปรอ้างอิงในเมธอดอาเรย์ 1 และอาร์เรย์ 2 ซึ่งชี้ไปที่อาร์เรย์ c และ b ตามลำดับ ไม่มีตัวแปรอ้างอิงที่ชี้ไปที่วัตถุ (อาร์เรย์) ก. ดังนั้นจึงมีสิทธิ์ได้รับการเก็บขยะ
คุณยังสามารถกำหนดค่าของ array2 ใน main ให้กับ array1 array1 จะเริ่มชี้ไปที่ b
ฉันมักจะคิดว่า "ส่งต่อสำเนา" เป็นสำเนาของค่าไม่ว่าจะเป็นแบบดั้งเดิมหรือการอ้างอิง ถ้ามันเป็นแบบดั้งเดิมมันคือสำเนาของบิตที่เป็นค่าและถ้ามันเป็นวัตถุมันก็คือสำเนาของการอ้างอิง
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:
ชื่อ = Maxx
ชื่อ = Fido
คลาสของ wrapper ดั้งเดิมและสตริงไม่เปลี่ยนรูปดังนั้นตัวอย่างใด ๆ ที่ใช้ประเภทเหล่านั้นจะไม่ทำงานเหมือนกับประเภท / อ็อบเจ็กต์อื่น ๆ
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();
}
ผมได้สร้างด้ายที่ทุ่มเทให้กับชนิดของคำถามเหล่านี้สำหรับการใด ๆการเขียนโปรแกรมภาษาที่นี่
Java นอกจากนี้ยังกล่าวถึง นี่คือสรุปสั้น ๆ :
- Java ส่งผ่านพารามิเตอร์ตามค่า
- "ตามค่า" เป็นวิธีเดียวใน java ในการส่งผ่านพารามิเตอร์ไปยังเมธอด
- การใช้วิธีการจากอ็อบเจ็กต์ที่กำหนดให้เป็นพารามิเตอร์จะเปลี่ยนอ็อบเจ็กต์เนื่องจากการอ้างอิงชี้ไปที่อ็อบเจ็กต์ดั้งเดิม (หากวิธีนั้นเปลี่ยนแปลงค่าบางอย่าง)
การแก้ไขบางโพสต์
C ไม่สนับสนุนการส่งผ่านโดยการอ้างอิง มันมักจะผ่านไปด้วยคุณค่า C ++ รองรับ pass by reference แต่ไม่ใช่ค่าเริ่มต้นและค่อนข้างอันตราย
ไม่สำคัญว่าค่าใน Java จะเป็นอย่างไร: ดั้งเดิมหรือแอดเดรส (โดยประมาณ) ของอ็อบเจ็กต์มันจะถูกส่งผ่านด้วยค่าเสมอ
หากออบเจ็กต์ Java "ทำงาน" เหมือนกำลังถูกส่งผ่านโดยการอ้างอิงนั่นเป็นคุณสมบัติของความไม่แน่นอนและไม่มีส่วนเกี่ยวข้องกับกลไกการส่งผ่าน
ฉันไม่แน่ใจว่าทำไมมันจึงสับสนมากอาจเป็นเพราะ "โปรแกรมเมอร์" Java จำนวนมากไม่ได้รับการฝึกฝนอย่างเป็นทางการจึงไม่เข้าใจว่าเกิดอะไรขึ้นในหน่วยความจำจริงๆ?