Là Java qua pass-by-Reference và hay pass-by-value-giá trị?


6580

Tôi luôn nghĩ Java là tài liệu tham khảo .

Tuy nhiên, tôi đã thấy một vài bài đăng trên blog (ví dụ: blog này ) cho rằng không phải vậy.

Tôi không nghĩ rằng tôi hiểu sự khác biệt mà họ đang tạo ra.

Giải thích là gì?


706
Tôi tin rằng phần lớn sự nhầm lẫn về vấn đề này có liên quan đến thực tế là những người khác nhau có định nghĩa khác nhau về thuật ngữ "tham chiếu". Những người đến từ nền C ++ cho rằng "tham chiếu" phải có nghĩa là gì trong C ++, những người từ nền C giả định "tham chiếu" phải giống như "con trỏ" trong ngôn ngữ của họ, v.v. Việc có đúng không khi nói rằng Java chuyển qua tham chiếu thực sự phụ thuộc vào ý nghĩa của "tham chiếu".
Trọng lực

119
Tôi cố gắng nhất quán sử dụng thuật ngữ được tìm thấy trong bài viết Chiến lược đánh giá . Cần lưu ý rằng, mặc dù bài báo chỉ ra các thuật ngữ khác nhau rất nhiều bởi cộng đồng, nó nhấn mạnh rằng ngữ nghĩa cho call-by-valuecall-by-referencekhác nhau theo một cách rất quan trọng . (Cá nhân tôi thích sử dụng call-by-object-sharingnhững ngày này hơn call-by-value[-of-the-reference], vì điều này mô tả ngữ nghĩa ở cấp độ cao và không tạo ra xung đột với call-by-value, đó triển khai cơ bản.)

59
@Gravity: Bạn có thể đi và đưa nhận xét của bạn lên một bảng quảng cáo HUGE hoặc một cái gì đó? Đó là toàn bộ vấn đề một cách ngắn gọn. Và nó cho thấy rằng toàn bộ điều này là ngữ nghĩa. Nếu chúng tôi không đồng ý với định nghĩa cơ bản của một tài liệu tham khảo, thì chúng tôi sẽ không đồng ý về câu trả lời cho câu hỏi này :)
MadConan

30
Tôi nghĩ rằng sự nhầm lẫn là "vượt qua tham chiếu" so với "ngữ nghĩa tham chiếu". Java là pass-by-value với ngữ nghĩa tham chiếu.
spraff

11
@Gravity, trong khi bạn hoàn toàn chính xác rằng những người đến từ C ++ theo bản năng sẽ có một trực giác khác về thuật ngữ "tham chiếu", cá nhân tôi tin rằng cơ thể bị chôn vùi nhiều hơn trong "bởi". "Vượt qua" khó hiểu ở chỗ nó hoàn toàn khác biệt với "Vượt qua" trong Java. Trong C ++, tuy nhiên thông thường thì không. Trong C ++, bạn có thể nói "vượt qua một tham chiếu" và nó hiểu rằng nó sẽ vượt qua swap(x,y)bài kiểm tra .

Câu trả lời:


5842

Java luôn luôn là giá trị truyền qua . Thật không may, khi chúng ta truyền giá trị của một đối tượng, chúng ta sẽ chuyển tham chiếu đến nó. Điều này gây nhầm lẫn cho người mới bắt đầu.

Nó như thế này:

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
}

Trong ví dụ trên aDog.getName()vẫn sẽ quay trở lại "Max". Giá trị aDogbên trong mainkhông được thay đổi trong hàm foovới Dog "Fifi"tham chiếu đối tượng được truyền theo giá trị. Nếu nó được chuyển qua tham chiếu, thì aDog.getName()in mainsẽ trở lại "Fifi"sau khi gọi đến foo.

Tương tự như vậy:

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

Trong ví dụ trên, Fifilà tên của con chó sau khi gọi đến foo(aDog)vì tên của đối tượng được đặt bên trong foo(...). Bất kỳ hoạt động nào foothực hiện trên dlà như vậy, cho tất cả các mục đích thực tế, chúng được thực hiện trên aDog, nhưng không thể thay đổi giá trị của aDogchính biến đó.


263
Có phải nó hơi khó hiểu vấn đề với các chi tiết nội bộ? Không có sự khác biệt về mặt khái niệm giữa 'truyền tham chiếu' và 'truyền giá trị của tham chiếu', giả sử rằng bạn có nghĩa là 'giá trị của con trỏ bên trong cho đối tượng'.
izb

383
Nhưng có một sự khác biệt tinh tế. Nhìn vào ví dụ đầu tiên. Nếu nó hoàn toàn được chuyển qua tham chiếu, aDog.name sẽ là "Fifi". Không phải vậy - tham chiếu bạn đang nhận là tham chiếu giá trị mà nếu ghi đè sẽ được khôi phục khi thoát khỏi chức năng.
erlando

300
@Lorenzo: Không, trong Java mọi thứ đều được truyền theo giá trị. Nguyên thủy được truyền theo giá trị và tham chiếu đối tượng được truyền theo giá trị. Bản thân các đối tượng không bao giờ được truyền cho một phương thức, nhưng các đối tượng luôn ở trong heap và chỉ một tham chiếu đến đối tượng được truyền cho phương thức.
Esko Luontola

294
Nỗ lực của tôi về một cách tốt để hình dung vật thể đi qua: Hãy tưởng tượng một quả bóng bay. Gọi một fxn giống như buộc một chuỗi thứ hai vào khinh khí cầu và đưa đường dây cho fxn. tham số = new Balloon () sẽ cắt chuỗi đó và tạo một quả bóng mới (nhưng điều này không có tác dụng với quả bóng ban đầu). tham số.pop () sẽ vẫn bật nó bởi vì nó theo chuỗi đến cùng một quả bóng ban đầu. Java được truyền theo giá trị, nhưng giá trị được truyền không sâu, nó ở mức cao nhất, tức là nguyên thủy hoặc con trỏ. Đừng nhầm lẫn rằng với giá trị truyền qua sâu, nơi đối tượng được nhân bản hoàn toàn và được thông qua.
dhackner

150
Điều khó hiểu là các tham chiếu đối tượng thực sự là con trỏ. Ban đầu, SUN gọi chúng là con trỏ. Sau đó, tiếp thị thông báo rằng "con trỏ" là một từ xấu. Nhưng bạn vẫn thấy danh pháp "chính xác" trong NullPulumException.
Hợp đồng của giáo sư Falken vi phạm

3035

Tôi chỉ nhận thấy bạn tham khảo bài viết của tôi .

Đặc tả Java nói rằng mọi thứ trong Java đều là giá trị truyền qua. Không có thứ gọi là "pass-by-Reference" trong Java.

Chìa khóa để hiểu điều này là một cái gì đó như

Dog myDog;

không một con chó; nó thực sự là một con trỏ đến một con chó.

Điều đó có nghĩa là gì khi bạn có

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

về cơ bản bạn đang chuyển địa chỉ của người được tạoDog đối tượng cho foophương thức.

(Tôi nói về cơ bản vì các con trỏ Java không có địa chỉ trực tiếp, nhưng dễ nhất là nghĩ về chúng theo cách đó)

Giả sử Dog đối tượng cư trú tại địa chỉ bộ nhớ 42. Điều này có nghĩa là chúng ta truyền 42 cho phương thức.

nếu Phương thức được định nghĩa là

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

Hãy nhìn vào những gì đang xảy ra.

  • tham số someDogđược đặt thành giá trị 42
  • tại dòng "AAA"
    • someDogđược theo sau Dognó trỏ đến ( Dogđối tượng tại địa chỉ 42)
    • rằng Dog(người ở địa chỉ 42) được yêu cầu đổi tên thành Max
  • tại dòng "BBB"
    • một cái mới Dogđược tạo ra Hãy nói rằng anh ấy ở địa chỉ 74
    • chúng ta gán tham số someDogcho 74
  • tại dòng "CCC"
    • someDog được theo sau để Dognó trỏ tới ( Dogđối tượng tại địa chỉ 74)
    • rằng Dog(người ở địa chỉ 74) được yêu cầu đổi tên thành Rowlf
  • sau đó, chúng tôi trở lại

Bây giờ hãy nghĩ về những gì xảy ra bên ngoài phương thức:

Đã myDogthay đổi?

Có chìa khóa.

Hãy nhớ rằng đó myDoglà một con trỏ , và không phải là một thực tế Dog, câu trả lời là KHÔNG. myDogvẫn có giá trị 42; nó vẫn chỉ vào bản gốc Dog(nhưng lưu ý rằng vì dòng "AAA", tên của nó bây giờ là "Max" - vẫn myDoglà giá trị của Dog; không thay đổi.)

Hoàn toàn hợp lệ để theo dõi một địa chỉ và thay đổi những gì ở cuối của nó; điều đó không thay đổi các biến, tuy nhiên.

Java hoạt động chính xác như C. Bạn có thể gán một con trỏ, chuyển con trỏ đến một phương thức, theo con trỏ trong phương thức và thay đổi dữ liệu được trỏ đến. Tuy nhiên, bạn không thể thay đổi nơi con trỏ trỏ tới.

Trong C ++, Ada, Pascal và các ngôn ngữ khác hỗ trợ tham chiếu qua, bạn thực sự có thể thay đổi biến đã được thông qua.

Nếu Java có ngữ nghĩa thông qua tham chiếu, thì foophương thức mà chúng ta đã định nghĩa ở trên sẽ thay đổi nơi myDogđược chỉ định khi được gán someDogtrên dòng BBB.

Hãy nghĩ về các tham số tham chiếu là bí danh cho biến được truyền vào. Khi bí danh đó được gán, thì biến đó được truyền vào.


164
Đây là lý do tại sao giới hạn chung "Java không có con trỏ" rất dễ gây hiểu lầm.
Beska

134
Bạn sai rồi, imho. "Hãy nhớ rằng myDog là một con trỏ và không phải là một con chó thực sự, câu trả lời là KHÔNG. MyDog vẫn có giá trị 42; nó vẫn chỉ vào Dog gốc." myDog có giá trị 42 nhưng đối số tên của nó hiện chứa "Max", thay vì "Rover" trên dòng // AAA.
Özgür

180
Hãy suy nghĩ về nó theo cách này. Ai đó có địa chỉ của Ann Arbor, MI (quê hương của tôi, GO BLUE!) Trên một tờ giấy gọi là "annArborLocation". Bạn sao chép nó xuống một mảnh giấy gọi là "myDestination". Bạn có thể lái xe đến "myDestination" và trồng cây. Bạn có thể đã thay đổi một cái gì đó về thành phố tại địa điểm đó, nhưng nó không thay đổi LAT / LON được viết trên một trong hai tờ giấy. Bạn có thể thay đổi LAT / LON trên "myDestination" nhưng nó không thay đổi "annArborLocation". cái đó có giúp ích không?
Scott Stanchfield

43
@Scott Stanchfield: Tôi đã đọc bài viết của bạn khoảng một năm trước và nó thực sự giúp tôi làm sáng tỏ mọi thứ. Cảm ơn! Tôi có thể khiêm tốn đề nghị một bổ sung nhỏ: bạn nên đề cập rằng thực sự có một thuật ngữ cụ thể mô tả hình thức "gọi theo giá trị này trong đó giá trị là một tham chiếu" được phát minh bởi Barbara Liskov để mô tả chiến lược đánh giá ngôn ngữ CLU của cô ấy trong 1974, để tránh nhầm lẫn, chẳng hạn như địa chỉ bài viết của bạn: gọi bằng cách chia sẻ (đôi khi được gọi bằng cách chia sẻ đối tượng hoặc đơn giản là gọi theo đối tượng ), mô tả khá chính xác ngữ nghĩa.
Jörg W Mittag

29
@Gevorg - Các ngôn ngữ C / C ++ không sở hữu khái niệm "con trỏ". Có những ngôn ngữ khác sử dụng con trỏ nhưng không cho phép các loại thao tác tương tự của con trỏ mà C / C ++ cho phép. Java có con trỏ; họ chỉ được bảo vệ chống lại sự nghịch ngợm.
Scott Stanchfield

1740

Java luôn truyền các đối số theo giá trị , KHÔNG theo tham chiếu.


Hãy để tôi giải thích điều này thông qua một ví dụ :

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

}

Tôi sẽ giải thích điều này trong các bước:

  1. Khai báo một tham chiếu có tên floại Foovà gán cho nó một đối tượng mới của loại Foovới một thuộc tính "f".

    Foo f = new Foo("f");

    nhập mô tả hình ảnh ở đây

  2. Từ phía phương thức, một tham chiếu kiểu Foocó tên ađược khai báo và nó được gán ban đầu null.

    public static void changeReference(Foo a)

    nhập mô tả hình ảnh ở đây

  3. Khi bạn gọi phương thức changeReference, tham chiếu asẽ được gán đối tượng được truyền dưới dạng đối số.

    changeReference(f);

    nhập mô tả hình ảnh ở đây

  4. Khai báo một tham chiếu có tên bloại Foovà gán cho nó một đối tượng mới của loại Foovới một thuộc tính "b".

    Foo b = new Foo("b");

    nhập mô tả hình ảnh ở đây

  5. a = bthực hiện một nhiệm vụ mới cho tham chiếu a, không phải f của đối tượng có thuộc tính của nó "b".

    nhập mô tả hình ảnh ở đây

  6. Khi bạn gọi modifyReference(Foo c)phương thức, một tham chiếu cđược tạo và gán đối tượng với thuộc tính "f".

    nhập mô tả hình ảnh ở đây

  7. c.setAttribute("c");sẽ thay đổi thuộc tính của đối tượng tham chiếu cđến nó và cùng một đối tượng tham chiếu fđến nó.

    nhập mô tả hình ảnh ở đây

Tôi hy vọng bạn hiểu cách chuyển các đối tượng làm đối số hoạt động trong Java :)


82
+1 thứ tốt đẹp. sơ đồ tốt. Tôi cũng tìm thấy một trang ngắn gọn đẹp ở đây adp-gmbh.ch/php/pass_by_Vference.html OK Tôi thừa nhận nó được viết bằng PHP, nhưng nó là nguyên tắc để hiểu sự khác biệt mà tôi nghĩ là quan trọng (và cách xử lý sự khác biệt đó bạn cần).
DaveM

5
@ Eng.Fouad Đó là một lời giải thích tốt đẹp, nhưng nếu ađiểm đến cùng một đối tượng như f(và không bao giờ nhận được bản sao riêng của mình của đối tượng fđiểm), bất kỳ thay đổi đối với đối tượng khiến sử dụng anên thay đổi flà tốt (vì chúng là cả hai làm việc với cùng một đối tượng ), vì vậy tại một số điểm aphải có bản sao riêng của đối tượng ftrỏ tới.
0x6C38

14
@MrD khi 'a' trỏ đến cùng một đối tượng 'f' cũng trỏ đến, thì mọi thay đổi được thực hiện cho đối tượng đó thông qua 'a' cũng có thể quan sát được thông qua 'f', NHƯNG NÓ KHÔNG THAY ĐỔI 'f'. 'f' vẫn trỏ đến cùng một đối tượng. Bạn hoàn toàn có thể thay đổi đối tượng, nhưng bạn không bao giờ có thể thay đổi những gì 'f' trỏ đến. Đây là vấn đề cơ bản mà vì một số lý do, một số người không thể hiểu được.
Mike Braun

@MikeBraun ... cái gì? Bây giờ bạn đã làm tôi bối rối: S. Không phải những gì bạn vừa viết trái ngược với những gì 6. và 7. cho thấy?
Máy giặt Evil

6
Đây là lời giải thích tốt nhất mà tôi đã tìm thấy. Nhân tiện, còn tình huống cơ bản thì sao? Ví dụ, đối số yêu cầu một kiểu int, nó vẫn truyền bản sao của một biến int cho đối số?
allenwang

732

Điều này sẽ cung cấp cho bạn một số hiểu biết về cách Java thực sự hoạt động đến mức trong cuộc thảo luận tiếp theo của bạn về việc chuyển Java qua tham chiếu hoặc chuyển qua giá trị bạn sẽ chỉ mỉm cười :-)

Bước một xin vui lòng xóa khỏi tâm trí của bạn từ bắt đầu bằng 'p' "_ _ _ _ _ _ _", đặc biệt nếu bạn đến từ các ngôn ngữ lập trình khác. Java và 'p' không thể được viết trong cùng một cuốn sách, diễn đàn hoặc thậm chí là txt.

Bước hai hãy nhớ rằng khi bạn truyền Đối tượng vào một phương thức, bạn sẽ truyền tham chiếu Đối tượng chứ không phải chính Đối tượng.

  • Sinh viên : Sư phụ, điều này có nghĩa là Java là tham chiếu qua?
  • Sư phụ : Châu chấu, số

Bây giờ hãy nghĩ về những gì tham chiếu / biến của Object làm / là:

  1. Một biến giữ các bit cho JVM biết cách đến đối tượng được tham chiếu trong bộ nhớ (Heap).
  2. Khi truyền đối số cho một phương thức, bạn KHÔNG truyền biến tham chiếu, nhưng là bản sao của các bit trong biến tham chiếu . Một cái gì đó như thế này: 3bad086a. 3bad086a đại diện cho một cách để đến được đối tượng đã qua.
  3. Vì vậy, bạn chỉ cần vượt qua 3bad086a rằng đó là giá trị của tài liệu tham khảo.
  4. Bạn đang chuyển giá trị của tham chiếu và không phải chính tham chiếu (và không phải đối tượng).
  5. Giá trị này thực sự được sao chép và đưa ra cho phương thức .

Trong phần sau đây (vui lòng không thử biên dịch / thực thi điều này ...):

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

Chuyện gì xảy ra

  • Biến người được tạo ra trong dòng # 1 và nó null ở đầu.
  • Một đối tượng Person mới được tạo ra trong dòng # 2, được lưu trữ trong bộ nhớ, và biến người được đưa ra tham chiếu đến đối tượng Person. Đó là, địa chỉ của nó. Hãy nói 3bad086a.
  • Biến người giữ địa chỉ của đối tượng được truyền cho hàm phù hợp # 3.
  • Trong dòng số 4, bạn có thể nghe âm thanh của sự im lặng
  • Kiểm tra nhận xét trên dòng # 5
  • Một biến cục bộ của phương thức - AnotherReferenceToTheSamePersonObject - được tạo và sau đó xuất hiện phép thuật trong dòng # 6:
    • Biến / người tham chiếu được sao chép từng bit một và được truyền cho người khácReferenceToTheSamePersonObject bên trong hàm.
    • Không có trường hợp mới của Người được tạo.
    • Cả " người " và " otherReferenceToTheSamePersonObject " đều có cùng giá trị là 3bad086a.
    • Đừng thử điều này nhưng người == otherReferenceToTheSamePersonObject sẽ đúng.
    • Cả hai biến đều có các bản sao IDENTICS của tham chiếu và cả hai đều tham chiếu đến cùng một đối tượng Person, đối tượng CÙNG trên Heap và KHÔNG phải là một bản sao.

Một bưc tranh đang gia ngan lơi noi:

Vượt qua giá trị

Lưu ý rằng các mũi tên khácReferenceToTheSamePersonObject được hướng vào đối tượng và không hướng tới người biến!

Nếu bạn không hiểu thì hãy tin tôi và nhớ rằng tốt hơn là nói Java vượt qua giá trị . Vâng, vượt qua bởi giá trị tham khảo . Oh tốt, thậm chí tốt hơn là pass-by-copy-of-the-biến-value! ;)

Bây giờ hãy thoải mái ghét tôi nhưng lưu ý rằng với điều này , không có sự khác biệt giữa việc truyền các kiểu dữ liệu nguyên thủy và Đối tượng khi nói về các đối số phương thức.

Bạn luôn luôn vượt qua một bản sao của các bit của giá trị của tham chiếu!

  • Nếu đó là kiểu dữ liệu nguyên thủy thì các bit này sẽ chứa giá trị của kiểu dữ liệu nguyên thủy.
  • Nếu đó là một đối tượng, các bit sẽ chứa giá trị của địa chỉ cho JVM biết cách đến đối tượng.

Java là giá trị truyền qua bởi vì bên trong một phương thức, bạn có thể sửa đổi Đối tượng được tham chiếu bao nhiêu tùy ý nhưng cho dù bạn có cố gắng đến đâu, bạn sẽ không bao giờ có thể sửa đổi biến đã qua sẽ tiếp tục tham chiếu (không phải p _ _ _ _ _ _ _) cùng một đối tượng không có vấn đề gì!


Hàm changeName ở trên sẽ không bao giờ có thể sửa đổi nội dung thực tế (các giá trị bit) của tham chiếu được truyền. Nói cách khác, ChangeName không thể khiến Person person tham chiếu đến Object khác.


Tất nhiên bạn có thể cắt ngắn và chỉ cần nói rằng Java là giá trị truyền qua!


5
Bạn có nghĩa là con trỏ? .. Nếu tôi hiểu chính xác, trong public void foo(Car car){ ... }, carlà cục bộ foovà nó chứa vị trí heap của Object? Vì vậy, nếu tôi thay đổi cargiá trị của car = new Car()nó, nó sẽ trỏ đến các đối tượng khác nhau trên heap? và nếu tôi thay đổi giá trị cartài sản theo car.Color = "Red", đối tượng trong đống được chỉ bởi carsẽ được sửa đổi. Ngoài ra, nó giống nhau trong C #? Xin hãy trả lời! Cảm ơn!
dpp

11
@domanokz Bạn đang giết tôi, xin đừng nói lại từ đó! ;) Lưu ý rằng tôi có thể đã trả lời câu hỏi này mà không cần nói 'tham khảo'. Đó là một vấn đề thuật ngữ và 'làm cho nó tồi tệ hơn. Tôi và Scot có quan điểm khác nhau về điều này không may. Tôi nghĩ rằng bạn đã có cách nó hoạt động trong Java, bây giờ bạn có thể gọi nó là pass-value, by-object-share, by-copy-of-the-biến-value, hoặc cảm thấy thoải mái khi nghĩ ra thứ khác! Tôi không thực sự quan tâm miễn là bạn hiểu cách thức hoạt động của nó và những gì trong một loại đối tượng: chỉ là một địa chỉ Hộp thư bưu điện! ;)
Marsellus Wallace

9
Âm thanh như bạn vừa thông qua một tài liệu tham khảo ? Tôi sẽ bảo vệ thực tế rằng Java vẫn là một ngôn ngữ tham chiếu được sao chép. Thực tế rằng nó là một tài liệu tham khảo sao chép không thay đổi thuật ngữ. Cả hai TÀI LIỆU THAM KHẢO vẫn chỉ đến cùng một đối tượng. Đây là một lập luận của chủ nghĩa thuần túy ...
John Strickler

3
Vì vậy, thả vào dòng lý thuyết # 9 System.out.println(person.getName());những gì sẽ hiển thị? "Tom" hay "Jerry"? Đây là điều cuối cùng sẽ giúp tôi vượt qua sự nhầm lẫn này.
TheBrenny

1
"Giá trị của tài liệu tham khảo " là những gì cuối cùng đã giải thích cho tôi.
Alexander Shubert

689

Java luôn luôn vượt qua giá trị, không có ngoại lệ, bao giờ hết .

Vậy làm thế nào mà mọi người có thể hoàn toàn bối rối vì điều này và tin rằng Java được truyền qua tham chiếu hoặc nghĩ rằng họ có một ví dụ về Java đóng vai trò là tham chiếu? Điểm mấu chốt là Java không bao giờ cung cấp quyền truy cập trực tiếp vào các giá trị của chính các đối tượng , trong mọi trường hợp. Quyền truy cập duy nhất vào các đối tượng là thông qua một tham chiếu đến đối tượng đó. Bởi vì các đối tượng Java luôn được truy cập thông qua một tham chiếu, thay vì trực tiếp, nên người ta thường nói về các trường và biến và đối số phương thứcđối tượng , khi chúng chỉ là sự nhầm lẫn bắt nguồn từ điều này (nói đúng, không chính xác) thay đổi về danh pháp. tham chiếu đến các đối tượng .

Vì vậy, khi gọi một phương thức

  • Đối với các đối số nguyên thủy ( int,, longv.v.), giá trị truyền qua là giá trị thực của nguyên thủy (ví dụ 3).
  • Đối với các đối tượng, pass by value là giá trị của tham chiếu đến đối tượng .

Vì vậy, nếu bạn có doSomething(foo)public void doSomething(Foo foo) { .. }hai Foos đã sao chép các tham chiếu trỏ đến cùng một đối tượng.

Đương nhiên, chuyển qua giá trị một tham chiếu đến một đối tượng trông rất giống (và không thể phân biệt trong thực tế từ) truyền một đối tượng bằng tham chiếu.


7
Vì các giá trị của nguyên thủy là bất biến (như Chuỗi), sự khác biệt giữa hai trường hợp không thực sự phù hợp.
Paŭlo Ebermann

4
Chính xác. Đối với tất cả những gì bạn có thể biết thông qua hành vi JVM có thể quan sát được, các nguyên hàm có thể được truyền qua tham chiếu và có thể sống trên đống. Họ không, nhưng điều đó thực sự không thể quan sát được bằng bất kỳ cách nào.
Trọng lực

6
nguyên thủy là bất biến? cái đó có mới trong Java 7 không?
dùng85421

4
Con trỏ là bất biến, nói chung là nguyên thủy. Chuỗi cũng không phải là nguyên thủy, nó là một đối tượng. Hơn nữa, cấu trúc cơ bản của Chuỗi là một mảng có thể thay đổi. Điều duy nhất bất biến về nó là độ dài, đó là bản chất vốn có của mảng.
kingfrito_5005

3
Đây là một câu trả lời khác chỉ ra bản chất phần lớn ngữ nghĩa của đối số. Định nghĩa của tham chiếu được cung cấp trong câu trả lời này sẽ làm cho Java "chuyển qua tham chiếu". Tác giả về cơ bản thừa nhận nhiều như vậy trong đoạn cuối bằng cách nói rằng nó "không thể phân biệt được trong thực tế" từ "tham chiếu qua". Tôi nghi ngờ OP đang hỏi vì mong muốn hiểu cách triển khai Java mà là để hiểu cách sử dụng Java một cách chính xác. Nếu nó không thể phân biệt được trong thực tế, thì sẽ không có gì phải quan tâm và thậm chí nghĩ về việc đó sẽ là một sự lãng phí thời gian.
Loduwijk

331

Java chuyển các tham chiếu theo giá trị.

Vì vậy, bạn không thể thay đổi tham chiếu được truyền vào.


27
Nhưng điều cứ lặp đi lặp lại "bạn không thể thay đổi giá trị của các đối tượng được truyền trong các đối số" rõ ràng là sai. Bạn có thể không thể khiến họ tham chiếu đến một đối tượng khác, nhưng bạn có thể thay đổi nội dung của họ bằng cách gọi phương thức của họ. IMO điều này có nghĩa là bạn mất tất cả các lợi ích của tài liệu tham khảo và không nhận được đảm bảo bổ sung.
Timmmm

34
Tôi chưa bao giờ nói "bạn không thể thay đổi giá trị của các đối tượng được truyền trong các đối số". Tôi sẽ nói "Bạn không thể thay đổi giá trị của tham chiếu đối tượng được truyền vào dưới dạng đối số phương thức", đây là một tuyên bố đúng về ngôn ngữ Java. Rõ ràng bạn có thể thay đổi trạng thái của đối tượng (miễn là nó không bất biến).
ScArcher2

20
Hãy nhớ rằng bạn thực sự không thể vượt qua các đối tượng trong java; các đối tượng ở lại trên đống. Con trỏ tới các đối tượng có thể được thông qua (được sao chép vào khung stack cho phương thức được gọi). Vì vậy, bạn không bao giờ thay đổi giá trị được truyền vào (con trỏ), nhưng bạn có thể tự do theo dõi nó và thay đổi điều trên đống mà nó trỏ tới. Đó là giá trị vượt qua.
Scott Stanchfield

10
Âm thanh như bạn vừa thông qua một tài liệu tham khảo ? Tôi sẽ bảo vệ thực tế rằng Java vẫn là một ngôn ngữ tham chiếu được sao chép. Thực tế rằng nó là một tài liệu tham khảo sao chép không thay đổi thuật ngữ. Cả hai TÀI LIỆU THAM KHẢO vẫn chỉ đến cùng một đối tượng. Đây là một lập luận của chủ nghĩa thuần túy ...
John Strickler

3
Java không truyền đối tượng, nó chuyển giá trị của con trỏ đến đối tượng. Điều này tạo ra một con trỏ mới đến vị trí bộ nhớ của đối tượng ban đầu trong một biến mới. Nếu bạn thay đổi giá trị (địa chỉ bộ nhớ mà nó trỏ đến) của biến con trỏ này trong một phương thức, thì con trỏ ban đầu được sử dụng trong trình gọi phương thức vẫn không thay đổi. Nếu bạn đang gọi tham số là tham chiếu thì thực tế đó là bản sao của tham chiếu gốc, không phải là tham chiếu ban đầu sao cho hiện có hai tham chiếu đến đối tượng có nghĩa là nó là giá trị truyền qua
theferrit32

238

Tôi cảm thấy tranh luận về "tham chiếu qua tham chiếu so với giá trị vượt qua" không phải là siêu hữu ích.

Nếu bạn nói, "Java là bất cứ điều gì (tham chiếu / giá trị)", trong cả hai trường hợp, bạn không cung cấp câu trả lời hoàn chỉnh. Dưới đây là một số thông tin bổ sung hy vọng sẽ giúp hiểu được những gì đang xảy ra trong bộ nhớ.

Khóa học sụp đổ trên stack / heap trước khi chúng ta bắt đầu triển khai Java: Các giá trị tiếp tục và tắt ngăn xếp theo một trật tự tốt đẹp, giống như một chồng đĩa ở quán ăn tự phục vụ. Bộ nhớ trong heap (còn được gọi là bộ nhớ động) là hỗn loạn và vô tổ chức. JVM chỉ cần tìm không gian bất cứ nơi nào có thể và giải phóng nó khi các biến sử dụng nó không còn cần thiết nữa.

Được chứ. Trước hết, người nguyên thủy địa phương đi trên ngăn xếp. Mã này:

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

kết quả trong việc này:

nguyên thủy trên ngăn xếp

Khi bạn khai báo và khởi tạo một đối tượng. Các đối tượng thực tế đi trên đống. Những gì đi trên ngăn xếp? Địa chỉ của đối tượng trên đống. Các lập trình viên C ++ sẽ gọi đây là một con trỏ, nhưng một số nhà phát triển Java lại chống lại từ "con trỏ". Bất cứ điều gì. Chỉ cần biết rằng địa chỉ của đối tượng đi trên ngăn xếp.

Thích như vậy:

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

ab * 7ch không phải một!

Một mảng là một đối tượng, vì vậy nó cũng đi vào heap. Và những gì về các đối tượng trong mảng? Họ có được không gian heap riêng và địa chỉ của từng đối tượng nằm trong mảng.

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

anh em marx

Vì vậy, những gì được thông qua khi bạn gọi một phương thức? Nếu bạn truyền vào một đối tượng, những gì bạn thực sự đi vào là địa chỉ của đối tượng. Một số người có thể nói "giá trị" của địa chỉ và một số người cho rằng đó chỉ là một tham chiếu đến đối tượng. Đây là nguồn gốc của cuộc chiến thần thánh giữa những người đề xướng "tham chiếu" và "giá trị". Những gì bạn gọi nó không quan trọng bằng việc bạn hiểu rằng những gì được truyền vào là địa chỉ cho đối tượng.

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

Một Chuỗi được tạo và không gian cho nó được phân bổ trong heap và địa chỉ của chuỗi được lưu trữ trên ngăn xếp và được cung cấp định danh hisName, vì địa chỉ của Chuỗi thứ hai giống như chuỗi đầu tiên, không có Chuỗi mới nào được tạo và không có không gian heap mới được phân bổ, nhưng một định danh mới được tạo trên ngăn xếp. Sau đó, chúng tôi gọi shout(): một khung ngăn xếp mới được tạo và một mã định danh mới, nameđược tạo và gán địa chỉ của Chuỗi đã tồn tại.

la da di da da da

Vậy, giá trị, tham khảo? Bạn nói "khoai tây".


7
Tuy nhiên, bạn nên theo dõi một ví dụ phức tạp hơn trong đó một hàm xuất hiện để thay đổi một biến thành địa chỉ mà nó có tham chiếu.
Brian Peterson

34
Mọi người không "nhảy múa xung quanh vấn đề thực sự" của stack vs heap, bởi vì đó không phải là vấn đề thực sự. Đó là một chi tiết thực hiện tốt nhất, và hết sức sai lầm ở điều tồi tệ nhất. (Đó là hoàn toàn có thể cho các đối tượng sống trên stack; google "phân tích thoát" Và một số lượng lớn của các đối tượng chứa nguyên thủy mà có lẽ. Không sống trên stack.) Vấn đề thực sự là chính xác sự khác biệt giữa các loại tài liệu tham khảo và các loại giá trị - đặc biệt, giá trị của biến kiểu tham chiếu là tham chiếu, không phải đối tượng mà nó đề cập.
cHao

8
Đó là một "chi tiết triển khai" trong đó Java không bao giờ được yêu cầu thực sự cho bạn thấy nơi một đối tượng sống trong bộ nhớ và trên thực tế dường như quyết tâm tránh rò rỉ thông tin đó. Nó có thể đặt đối tượng lên ngăn xếp, và bạn sẽ không bao giờ biết. Nếu bạn quan tâm, bạn đang tập trung vào điều sai - và trong trường hợp này, điều đó có nghĩa là bỏ qua vấn đề thực sự.
cHao

10
Và dù bằng cách nào, "nguyên thủy đi vào ngăn xếp" là không chính xác. Các biến cục bộ nguyên thủy đi trên ngăn xếp. (Tất nhiên, nếu chúng không được tối ưu hóa đi.) Nhưng sau đó, các biến tham chiếu cục bộ cũng vậy . Và các thành viên nguyên thủy được xác định trong một đối tượng sống bất cứ nơi nào đối tượng sống.
cHao

9
Đồng ý với ý kiến ​​ở đây. Stack / heap là một vấn đề phụ, và không liên quan. Một số biến có thể nằm trên ngăn xếp, một số biến trong bộ nhớ tĩnh (biến tĩnh) và nhiều biến trực tiếp trên heap (tất cả các biến thành viên đối tượng). Không thể thông qua các biến này bằng cách tham chiếu: từ một phương thức được gọi, KHÔNG BAO GIỜ có thể thay đổi giá trị của một biến được truyền dưới dạng đối số. Do đó, không có tham chiếu qua trong Java.

195

Chỉ để hiển thị độ tương phản, so sánh các đoạn C ++Java sau đây :

Trong C ++: Lưu ý: Mã xấu - rò rỉ bộ nhớ! Nhưng nó thể hiện quan điểm.

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

Trong 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 chỉ có hai loại truyền: theo giá trị cho các loại dựng sẵn và theo giá trị của con trỏ cho các loại đối tượng.


7
+1 Tôi cũng sẽ thêm Dog **objPtrPtrvào ví dụ C ++, theo cách đó chúng ta có thể sửa đổi những gì con trỏ "trỏ đến".
Amro

1
Nhưng điều này không trả lời cho câu hỏi đã cho trong Java. Thực tế, mọi thứ trong phương thức của Java là Pass By Value, không có gì hơn thế.
tauitdnmd

2
Ví dụ này chỉ chứng minh rằng java sử dụng chính con trỏ trong C không? Trường hợp vượt qua giá trị trong C về cơ bản chỉ được đọc? Cuối cùng, toàn bộ chủ đề này là không thể hiểu được
amdev

179

Java chuyển các tham chiếu đến các đối tượng theo giá trị.


đây là câu trả lời tốt nhất Ngắn gọn và chính xác.
woens

166

Về cơ bản, việc gán lại các tham số Object không ảnh hưởng đến đối số, ví dụ:

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

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

sẽ in ra "Hah!"thay vì null. Lý do điều này hoạt động là vì barlà một bản sao của giá trị baz, mà chỉ là một tài liệu tham khảo "Hah!". Nếu nó là những tài liệu tham khảo thực tế bản thân, sau đó foosẽ định nghĩa lại bazđể null.


8
Tôi muốn nói rằng thanh đó là một bản sao của baz tham chiếu (hoặc bí danh baz), ban đầu chỉ vào cùng một đối tượng.
MaxZoom

không có một chút khác biệt giữa lớp String và tất cả các lớp khác?
Mehdi Karamosly

152

Tôi không thể tin rằng không ai nhắc đến Barbara Liskov. Khi cô thiết kế CLU vào năm 1974, cô gặp phải vấn đề thuật ngữ tương tự, và cô đã phát minh ra thuật ngữ này cuộc gọi bằng cách chia sẻ (còn được gọi là chia sẻ đối tượnggọi theo đối tượng ) cho trường hợp cụ thể này là "gọi theo giá trị trong đó giá trị là một tài liệu tham khảo".


3
Tôi thích sự khác biệt này trong danh pháp. Thật không may khi Java hỗ trợ cuộc gọi bằng cách chia sẻ cho các đối tượng, nhưng không gọi theo giá trị (cũng như C ++). Java chỉ hỗ trợ gọi theo giá trị cho các loại dữ liệu nguyên thủy và không phải các loại dữ liệu tổng hợp.
Derek Mahar

2
Tôi thực sự không nghĩ rằng chúng tôi cần một thuật ngữ bổ sung - nó chỉ đơn giản là vượt qua giá trị cho một loại giá trị cụ thể. Sẽ thêm "cuộc gọi bằng nguyên thủy" thêm bất kỳ làm rõ?
Scott Stanchfield


Vì vậy, tôi có thể chuyển qua tham chiếu bằng cách chia sẻ một số đối tượng bối cảnh toàn cầu hoặc thậm chí chuyển một đối tượng bối cảnh có chứa các tham chiếu khác? Tôi vẫn truyền đạt giá trị, nhưng ít nhất tôi có quyền truy cập vào các tài liệu tham khảo mà tôi có thể sửa đổi và làm cho chúng trỏ đến một cái gì đó khác.
YoYo

1
@Sanjeev: chia sẻ cuộc gọi theo đối tượng là một trường hợp đặc biệt của giá trị truyền qua. Tuy nhiên, rất nhiều người tranh luận kịch liệt rằng Java (và các ngôn ngữ tương tự như Python, Ruby, ECMAScript, Smalltalk) là tham chiếu qua. Tôi cũng thích gọi nó là pass-by-value, nhưng chia sẻ cuộc gọi theo đối tượng dường như là một thuật ngữ hợp lý mà ngay cả những người cho rằng nó không phải là giá trị truyền qua có thể đồng ý.
Jörg W Mittag

119

Mấu chốt của vấn đề là tham chiếu từ trong cụm từ "vượt qua tham chiếu" có nghĩa là một cái gì đó hoàn toàn khác với nghĩa thông thường của tham chiếu từ trong Java.

Thông thường trong tham chiếu Java có nghĩa là aa tham chiếu đến một đối tượng . Nhưng các thuật ngữ kỹ thuật vượt qua tham chiếu / giá trị từ lý thuyết ngôn ngữ lập trình đang nói về một tham chiếu đến ô nhớ giữ biến , đây là một điều hoàn toàn khác.


7
@Gevorg - Vậy "NullPulumException" là gì?
Hot Licks

5
@Hot: Một ngoại lệ được đặt tên không may từ trước khi Java giải quyết một thuật ngữ rõ ràng. Ngoại lệ tương đương về mặt ngữ nghĩa trong c # được gọi là NullReferenceException.
JacquesB

4
Dường như đối với tôi, việc sử dụng "tham chiếu" trong thuật ngữ Java là một ảnh hưởng cản trở sự hiểu biết.
Hot Licks

Tôi đã học cách gọi những cái gọi là "con trỏ tới đối tượng" là "xử lý đối tượng". Điều này làm giảm sự mơ hồ.
moronkreacionz

86

Trong java mọi thứ đều là tham chiếu, vì vậy khi bạn có một cái gì đó như: Point pnt1 = new Point(0,0); Java thực hiện như sau:

  1. Tạo đối tượng Point mới
  2. Tạo tham chiếu Điểm mới và khởi tạo tham chiếu đó đến điểm (tham chiếu) trên đối tượng Điểm được tạo trước đó.
  3. Từ đây, thông qua cuộc sống đối tượng Point, bạn sẽ truy cập vào đối tượng đó thông qua tham chiếu pnt1. Vì vậy, chúng ta có thể nói rằng trong Java, bạn thao tác đối tượng thông qua tham chiếu của nó.

nhập mô tả hình ảnh ở đây

Java không vượt qua các đối số phương thức bằng tham chiếu; nó vượt qua chúng bằng giá trị. Tôi sẽ sử dụng ví dụ từ trang web này :

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

Dòng chảy của chương trình:

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

Tạo hai đối tượng Điểm khác nhau với hai tham chiếu khác nhau được liên kết. nhập mô tả hình ảnh ở đây

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

Theo dự kiến, sản lượng sẽ là:

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

Trên dòng này, 'pass-by-value' đi vào vở kịch ...

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

Các tham chiếu pnt1pnt2được truyền theo giá trị cho phương thức phức tạp, có nghĩa là bây giờ các tham chiếu của bạn pnt1pnt2copiestên arg1arg2.So của chúng pnt1arg1 trỏ đến cùng một đối tượng. (Tương tự cho pnt2arg2) nhập mô tả hình ảnh ở đây

Trong trickyphương thức:

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

nhập mô tả hình ảnh ở đây

Tiếp theo trong trickyphương thức

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

Ở đây, trước tiên bạn tạo temptham chiếu Điểm mới sẽ trỏ vào cùng một vị trí như arg1tham chiếu. Sau đó, bạn di chuyển tham chiếu arg1để trỏ đến cùng một nơi như arg2tham chiếu. Cuối cùng arg2sẽ chỉ đến cùng một nơi như thế temp.

nhập mô tả hình ảnh ở đây

Từ đây phạm vi của trickyphương pháp đã biến mất và bạn không có quyền truy cập bất kỳ hơn với tài liệu tham khảo: arg1, arg2, temp. Nhưng lưu ý quan trọng là mọi thứ bạn làm với các tham chiếu này khi chúng 'trong cuộc sống' sẽ ảnh hưởng vĩnh viễn đến đối tượng mà chúng hướng đến.

Vì vậy, sau khi thực hiện phương thức tricky, khi bạn quay lại main, bạn có tình huống này: nhập mô tả hình ảnh ở đây

Vì vậy, bây giờ, thực hiện hoàn toàn chương trình sẽ là:

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

7
Trong java khi bạn nói "Trong java mọi thứ đều là tham chiếu", bạn có nghĩa là tất cả các đối tượng được truyền bằng tham chiếu. Các loại dữ liệu nguyên thủy không được thông qua tham chiếu.
Eric

Tôi có thể in giá trị hoán đổi trong phương thức chính. trong phương thức trickey, hãy thêm câu lệnh sau arg1.x = 1; arg1.y = 1; arg2.x = 2; arg2.y = 2;, vì arg1 hiện đang giữ pnt2 Revence và arg2 đang giữ tham chiếu pnt1, vì vậy, in ấn của nóX1: 2 Y1: 2 X2: 1 Y2: 1
Shahid Ghafoor

85

Java luôn được truyền theo giá trị, không truyền qua tham chiếu

Trước hết, chúng ta cần hiểu những gì vượt qua giá trị và vượt qua tham chiếu là gì.

Truyền theo giá trị có nghĩa là bạn đang tạo một bản sao trong bộ nhớ của giá trị tham số thực được truyền vào. Đây là bản sao nội dung của tham số thực .

Truyền theo tham chiếu (còn được gọi là truyền theo địa chỉ) có nghĩa là bản sao địa chỉ của tham số thực được lưu trữ .

Đôi khi Java có thể tạo ảo giác về việc vượt qua bằng cách tham chiếu. Hãy xem cách nó hoạt động bằng cách sử dụng ví dụ dưới đây:

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

Đầu ra của chương trình này là:

changevalue

Hãy hiểu từng bước:

Test t = new Test();

Như chúng ta đều biết nó sẽ tạo một đối tượng trong heap và trả giá trị tham chiếu trở lại t. Ví dụ: giả sử giá trị của t là 0x100234(chúng ta không biết giá trị bên trong JVM thực tế, đây chỉ là một ví dụ).

minh họa đầu tiên

new PassByValue().changeValue(t);

Khi truyền tham chiếu t cho hàm, nó sẽ không trực tiếp chuyển giá trị tham chiếu thực tế của kiểm tra đối tượng, nhưng nó sẽ tạo một bản sao của t và sau đó chuyển nó đến hàm. Vì nó được truyền theo giá trị , nó chuyển một bản sao của biến chứ không phải là tham chiếu thực tế của nó. Vì chúng ta đã nói giá trị của t là 0x100234, cả t và f sẽ có cùng giá trị và do đó chúng sẽ trỏ đến cùng một đối tượng.

minh họa thứ hai

Nếu bạn thay đổi bất cứ điều gì trong hàm bằng tham chiếu f, nó sẽ sửa đổi nội dung hiện có của đối tượng. Đó là lý do tại sao chúng ta có đầu ra changevalue, được cập nhật trong hàm.

Để hiểu rõ hơn về điều này, hãy xem xét ví dụ sau:

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

Điều này sẽ ném một NullPointerException? Không, bởi vì nó chỉ vượt qua một bản sao của tài liệu tham khảo. Trong trường hợp chuyển qua tham chiếu, nó có thể đã ném một NullPointerException, như được thấy dưới đây:

minh họa thứ ba

Hy vọng điều này sẽ giúp.


71

Một tài liệu tham khảo luôn là một giá trị khi được đại diện, bất kể bạn sử dụng ngôn ngữ nào.

Để có một khung nhìn bên ngoài hộp, hãy xem hội hoặc một số quản lý bộ nhớ cấp thấp. Ở cấp độ CPU, một tham chiếu đến bất cứ thứ gì ngay lập tức trở thành một giá trị nếu nó được ghi vào bộ nhớ hoặc một trong các thanh ghi CPU. (Đó là lý do tại sao con trỏ là một định nghĩa tốt. Nó là một giá trị, có mục đích cùng một lúc).

Dữ liệu trong bộ nhớ có Vị trí và tại vị trí đó có một giá trị (byte, từ, bất cứ thứ gì). Trong hội, chúng tôi có một giải pháp thuận tiện để đặt Tên cho một vị trí nhất định (còn gọi là biến), nhưng khi biên dịch mã, trình biên dịch chỉ cần thay thế Tên bằng vị trí được chỉ định giống như trình duyệt của bạn thay thế tên miền bằng địa chỉ IP.

Về cốt lõi, về mặt kỹ thuật không thể chuyển một tham chiếu đến bất kỳ thứ gì trong bất kỳ ngôn ngữ nào mà không đại diện cho nó (khi nó ngay lập tức trở thành một giá trị).

Hãy nói rằng chúng ta có một biến Foo, Vị trí của nó nằm ở byte thứ 47 trong bộ nhớ và Giá trị của nó là 5. Chúng ta có một biến Ref2Foo khác ở byte thứ 223 và giá trị của nó sẽ là 47. Ref2Foo này có thể là biến kỹ thuật , không được tạo ra rõ ràng bởi chương trình. Nếu bạn chỉ nhìn vào 5 và 47 mà không có bất kỳ thông tin nào khác, bạn sẽ chỉ thấy hai Giá trị . Nếu bạn sử dụng chúng làm tài liệu tham khảo thì để tiếp cận 5chúng tôi phải đi du lịch:

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

Đây là cách bàn nhảy hoạt động.

Nếu chúng ta muốn gọi một phương thức / hàm / thủ tục với giá trị của Foo, có một vài cách có thể để truyền biến cho phương thức, tùy thuộc vào ngôn ngữ và một số chế độ gọi phương thức của nó:

  1. 5 được sao chép vào một trong các thanh ghi CPU (ví dụ: EAX).
  2. 5 được PUSHd vào ngăn xếp.
  3. 47 được sao chép vào một trong các thanh ghi CPU
  4. 47 ĐÚNG vào ngăn xếp.
  5. 223 được sao chép vào một trong các thanh ghi CPU.
  6. 223 được PUSHd vào ngăn xếp.

Trong mọi trường hợp trên một giá trị - một bản sao của một giá trị hiện tại - đã được tạo, giờ đây tùy thuộc vào phương thức nhận để xử lý nó. Khi bạn viết "Foo" bên trong phương thức, nó sẽ được đọc từ EAX hoặc được tự động hủy đăng ký hoặc được nhân đôi, quá trình này phụ thuộc vào cách ngôn ngữ hoạt động và / hoặc loại Foo ra lệnh. Điều này được ẩn từ nhà phát triển cho đến khi cô ấy phá vỡ quy trình hội nghị. Vì vậy, một tài liệu tham khảo là một giá trị khi được biểu diễn, bởi vì một tham chiếu là một giá trị phải được xử lý (ở cấp độ ngôn ngữ).

Bây giờ chúng ta đã truyền Foo cho phương thức:

  • trong trường hợp 1. và 2. nếu bạn thay đổi Foo ( Foo = 9), nó chỉ ảnh hưởng đến phạm vi cục bộ khi bạn có một bản sao của Giá trị. Từ bên trong phương thức, chúng ta thậm chí không thể xác định được vị trí của bộ nhớ Foo ban đầu.
  • trong trường hợp 3. và 4. nếu bạn sử dụng các cấu trúc ngôn ngữ mặc định và thay đổi Foo ( Foo = 11), nó có thể thay đổi Foo trên toàn cầu (phụ thuộc vào ngôn ngữ, ví dụ: Java hoặc như procedure findMin(x, y, z: integer;var m của Pascal : integer);). Tuy nhiên, nếu ngôn ngữ cho phép bạn phá vỡ quy trình bãi bỏ quy định, bạn có thể thay đổi 47, nói với 49. Tại thời điểm đó, Foo dường như đã bị thay đổi nếu bạn đọc nó, bởi vì bạn đã thay đổi con trỏ cục bộ thành nó. Và nếu bạn đã sửa đổi Foo này bên trong phương thức ( Foo = 12), bạn có thể sẽ FUBAR thực thi chương trình (hay còn gọi là segfault) vì bạn sẽ ghi vào một bộ nhớ khác so với dự kiến, thậm chí bạn có thể sửa đổi một khu vực được định sẵn để thực thi chương trình và viết vào nó sẽ sửa đổi mã đang chạy (Foo hiện không hoạt động 47). NHƯNG giá trị của Foo là47không thay đổi toàn cầu, chỉ có một bên trong phương thức, vì 47đó cũng là một bản sao của phương thức.
  • trong trường hợp 5. và 6. nếu bạn sửa đổi 223bên trong phương thức, nó sẽ tạo ra tình trạng lộn xộn giống như trong 3. hoặc 4. (một con trỏ, trỏ đến một giá trị xấu bây giờ, lại được sử dụng như một con trỏ) nhưng đây vẫn là một cục bộ vấn đề, như 223 đã được sao chép . Tuy nhiên, nếu bạn có thể hủy đăng ký Ref2Foo(nghĩa là 223), tiếp cận và sửa đổi giá trị nhọn 47, giả sử 49, nó sẽ ảnh hưởng đến Foo trên toàn cầu , vì trong trường hợp này, các phương thức có một bản sao 223 nhưng tham chiếu 47chỉ tồn tại một lần và thay đổi điều đó để 49sẽ dẫn mỗi Ref2Foođúp dereferencing đến một giá trị sai.

Nitpicking trên các chi tiết không đáng kể, ngay cả các ngôn ngữ tham chiếu qua sẽ chuyển các giá trị cho các hàm, nhưng các hàm đó biết rằng chúng phải sử dụng nó cho mục đích hội nghị. Giá trị pass-the-Reference-as-value này chỉ bị ẩn khỏi lập trình viên bởi vì nó thực sự vô dụng và thuật ngữ chỉ là tham chiếu qua .

Truyền giá trị nghiêm ngặt cũng vô dụng, điều đó có nghĩa là một mảng 100 Mbyte phải được sao chép mỗi khi chúng ta gọi một phương thức với mảng là đối số, do đó Java không thể là giá trị truyền qua một cách nghiêm ngặt. Mọi ngôn ngữ sẽ chuyển tham chiếu đến mảng lớn này (dưới dạng giá trị) và sử dụng cơ chế sao chép trên ghi nếu mảng đó có thể được thay đổi cục bộ bên trong phương thức hoặc cho phép phương thức (như Java thực hiện) để sửa đổi mảng trên toàn cầu (từ chế độ xem của người gọi) và một vài ngôn ngữ cho phép sửa đổi Giá trị của chính tham chiếu.

Vì vậy, trong ngắn hạn và theo thuật ngữ riêng của Java, Java là giá trị truyền qua trong đó giá trị có thể là: giá trị thực hoặc giá trị đại diện cho tham chiếu .


1
Trong một ngôn ngữ có tham chiếu qua, điều được thông qua (tham chiếu) là phù du; người nhận không được "cho là" sao chép nó. Trong Java, việc truyền một mảng được truyền là "định danh đối tượng" - tương đương với một tờ giấy có nội dung "Đối tượng # 24601", khi đối tượng thứ 24601 được xây dựng là một mảng. Người nhận có thể sao chép "Đối tượng # 24601" bất cứ nơi nào họ muốn và bất kỳ ai có một tờ giấy ghi "Đối tượng # 24601" có thể làm bất cứ điều gì họ muốn với bất kỳ thành phần mảng nào. Tất nhiên, mô hình của các bit được truyền sẽ không thực sự nói "Object # 24601", nhưng ...
supercat

... điểm mấu chốt là người nhận id đối tượng xác định mảng có thể lưu trữ id đối tượng đó bất cứ nơi nào họ muốn và cung cấp cho bất kỳ ai muốn, và bất kỳ người nhận nào cũng có thể truy cập hoặc sửa đổi mảng bất cứ khi nào muốn Ngược lại, nếu một mảng được truyền bằng tham chiếu trong một ngôn ngữ như Pascal hỗ trợ những thứ đó, thì phương thức được gọi có thể làm bất cứ điều gì nó muốn với mảng, nhưng không thể lưu trữ tham chiếu theo cách để cho phép mã sửa đổi mảng sau khi nó trở lại
supercat

Thật vậy, trong Pascal, bạn có thể lấy địa chỉ của mọi biến, được chuyển qua tham chiếu hoặc sao chép cục bộ, addrkhông được @gõ, thực hiện với loại và bạn có thể sửa đổi biến được tham chiếu sau (ngoại trừ biến được sao chép cục bộ). Nhưng tôi không thấy lý do tại sao bạn sẽ làm điều đó. Tờ giấy đó trong ví dụ của bạn (Object # 24601) là một tài liệu tham khảo, mục đích của nó là giúp tìm mảng trong bộ nhớ, nó không chứa bất kỳ dữ liệu mảng nào trong chính nó. Nếu bạn khởi động lại chương trình của mình, cùng một mảng có thể nhận được một id đối tượng khác ngay cả khi nội dung của nó sẽ giống như trong lần chạy trước.
karatedog

Tôi đã nghĩ toán tử "@" không phải là một phần của Pascal tiêu chuẩn, nhưng được triển khai như một phần mở rộng chung. Nó là một phần của tiêu chuẩn? Quan điểm của tôi là trong một ngôn ngữ có mật khẩu thực sự và không có khả năng xây dựng một con trỏ không phù du cho một đối tượng phù du, mã giữ tham chiếu duy nhất đến một mảng, ở bất cứ đâu trong vũ trụ, trước khi đi qua mảng tham chiếu có thể biết rằng trừ khi người nhận "gian lận", nó vẫn sẽ giữ tham chiếu duy nhất sau đó. Cách an toàn duy nhất để thực hiện điều đó trong Java là xây dựng một đối tượng tạm thời ...
supercat

... trong đó gói gọn một AtomicReferencevà không để lộ tham chiếu cũng như mục tiêu của nó, mà thay vào đó bao gồm các phương thức để thực hiện mọi thứ cho mục tiêu; một khi mã mà đối tượng được truyền trở lại, AtomicReference[mà tác giả của nó giữ tham chiếu trực tiếp] sẽ bị vô hiệu và bị bỏ qua. Điều đó sẽ cung cấp ngữ nghĩa thích hợp, nhưng nó sẽ chậm và khó hiểu.
supercat

70

Java là một cuộc gọi theo giá trị

Làm thế nào nó hoạt động

  • Bạn luôn luôn vượt qua một bản sao của các bit của giá trị của tham chiếu!

  • Nếu đó là kiểu dữ liệu nguyên thủy thì các bit này chứa giá trị của chính kiểu dữ liệu nguyên thủy, đó là lý do tại sao nếu chúng ta thay đổi giá trị của tiêu đề bên trong phương thức thì nó không phản ánh các thay đổi bên ngoài.

  • Nếu đó là kiểu dữ liệu đối tượng như Foo foo = new Foo () thì trong trường hợp này, bản sao địa chỉ của đối tượng chuyển như phím tắt tệp, giả sử chúng ta có tệp văn bản abc.txt tại C: \ desktop và giả sử chúng ta tạo lối tắt cùng một tệp và đặt tệp này vào trong C: \ desktop \ abc-phím tắt để khi bạn truy cập tệp từ C: \ desktop \ abc.txt và viết 'Stack Overflow' và đóng tệp và bạn lại mở tệp từ phím tắt write 'là cộng đồng trực tuyến lớn nhất để lập trình viên tìm hiểu', sau đó tổng số thay đổi tệp sẽ có 'Stack Overflow là cộng đồng trực tuyến lớn nhất để lập trình viên học hỏi'nghĩa là không quan trọng bạn mở tệp ở đâu, mỗi lần chúng tôi truy cập cùng một tệp, ở đây chúng tôi có thể giả sửFoo dưới dạng tệp và giả sử foo được lưu trữ tại 123hd7h (địa chỉ gốc như địa chỉ C: \ desktop \ abc.txt ) và 234jdid (địa chỉ được sao chép như C: \ desktop \ abc-phím tắt thực sự chứa địa chỉ gốc của tệp bên trong). Vì vậy, để hiểu rõ hơn hãy tạo tập tin phím tắt và cảm nhận ..


66

Đã có câu trả lời tuyệt vời bao gồm điều này. Tôi muốn đóng góp nhỏ bằng cách chia sẻ một ví dụ rất đơn giản (sẽ biên dịch) tương phản các hành vi giữa Pass-by-Reference trong c ++ và Pass-by-value trong Java.

Một vài điểm:

  1. Thuật ngữ "tham chiếu" là một quá tải với hai ý nghĩa riêng biệt. Trong Java, nó chỉ đơn giản có nghĩa là một con trỏ, nhưng trong ngữ cảnh "Truyền qua tham chiếu", nó có nghĩa là một điều khiển cho biến ban đầu được truyền vào.
  2. Java là Pass-by-value . Java là hậu duệ của C (trong số các ngôn ngữ khác). Trước C, một số ngôn ngữ trước đây (nhưng không phải tất cả) như FORTRAN và COBOL hỗ trợ PBR, nhưng C thì không. PBR cho phép các ngôn ngữ khác thực hiện thay đổi đối với các biến được truyền trong các thường trình con. Để thực hiện điều tương tự (nghĩa là thay đổi giá trị của các biến bên trong các hàm), các lập trình viên C đã chuyển các con trỏ tới các biến thành các hàm. Các ngôn ngữ được lấy cảm hứng từ C, chẳng hạn như Java, đã mượn ý tưởng này và tiếp tục chuyển con trỏ đến các phương thức như C đã làm, ngoại trừ việc Java gọi các tham chiếu con trỏ của nó. Một lần nữa, đây là cách sử dụng khác của từ "Tham chiếu" so với "Tham chiếu qua".
  3. C ++ cho phép Pass-by-Reference bằng cách khai báo một tham số tham chiếu bằng cách sử dụng ký tự "&" (có thể là cùng một ký tự được sử dụng để chỉ ra "địa chỉ của một biến" trong cả C và C ++). Ví dụ: nếu chúng ta truyền vào một con trỏ bằng tham chiếu, tham số và đối số không chỉ trỏ đến cùng một đối tượng. Thay vào đó, chúng là cùng một biến. Nếu một cái được đặt thành một địa chỉ khác hoặc thành null, thì cái kia cũng vậy.
  4. Trong ví dụ C ++ bên dưới, tôi chuyển một con trỏ tới một chuỗi kết thúc null bằng tham chiếu . Và trong ví dụ Java bên dưới, tôi chuyển một tham chiếu Java tới một Chuỗi (một lần nữa, giống như một con trỏ tới Chuỗi) theo giá trị. Lưu ý đầu ra trong các ý kiến.

C ++ vượt qua ví dụ tham chiếu:

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 vượt qua "một tham chiếu Java" bằng ví dụ giá trị

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

BIÊN TẬP

Một số người đã viết bình luận dường như chỉ ra rằng họ không nhìn vào ví dụ của tôi hoặc họ không lấy ví dụ về c ++. Không chắc chắn nơi ngắt kết nối, nhưng đoán ví dụ c ++ không rõ ràng. Tôi đang đăng cùng một ví dụ trong pascal vì tôi nghĩ rằng tham chiếu qua có vẻ sạch hơn trong pascal, nhưng tôi có thể sai. Tôi có thể chỉ khiến mọi người bối rối hơn; Tôi hy vọng là không.

Trong pascal, các tham số truyền qua tham chiếu được gọi là "tham số var". Trong quy trình setToNil bên dưới, vui lòng lưu ý từ khóa 'var' có trước tham số 'ptr'. Khi một con trỏ được truyền cho thủ tục này, nó sẽ được truyền bằng tham chiếu . Lưu ý hành vi: khi quy trình này đặt ptr thành nil (pascal nói cho NULL), nó sẽ đặt đối số thành nil - bạn không thể làm điều đó trong 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.

CHỈNH SỬA 2

Một số trích đoạn từ "Ngôn ngữ lập trình Java" của Ken Arnold, James Gosling (người đã phát minh ra Java) và David Holmes, chương 2, mục 2.6.5

Tất cả các tham số cho các phương thức được truyền "theo giá trị" . Nói cách khác, các giá trị của các biến tham số trong một phương thức là các bản sao của invoker được chỉ định làm đối số.

Anh ta tiếp tục đưa ra quan điểm tương tự về các đồ vật. . .

Bạn nên lưu ý rằng khi tham số là tham chiếu đối tượng, thì đó là tham chiếu đối tượng - không phải chính đối tượng - được truyền "theo giá trị" .

Và đến cuối cùng của phần đó, anh ta đưa ra một tuyên bố rộng hơn về java chỉ được truyền theo giá trị và không bao giờ vượt qua tham chiếu.

Ngôn ngữ lập trình Java không vượt qua các đối tượng bằng cách tham chiếu; nó vượt qua các tham chiếu đối tượng theo giá trị . Bởi vì hai bản sao của cùng một tham chiếu tham chiếu đến cùng một đối tượng thực tế, những thay đổi được thực hiện thông qua một biến tham chiếu được hiển thị thông qua biến khác. Có chính xác một tham số chuyển chế độ - truyền theo giá trị - và giúp mọi thứ đơn giản.

Phần này của cuốn sách có một lời giải thích tuyệt vời về việc truyền tham số trong Java và về sự khác biệt giữa thông qua tham chiếu và thông qua giá trị và nó do người tạo ra Java. Tôi sẽ khuyến khích bất cứ ai đọc nó, đặc biệt là nếu bạn vẫn chưa bị thuyết phục.

Tôi nghĩ rằng sự khác biệt giữa hai mô hình là rất tinh tế và trừ khi bạn đã thực hiện lập trình nơi bạn thực sự sử dụng tham chiếu qua, rất dễ bỏ lỡ hai mô hình khác nhau.

Tôi hy vọng điều này giải quyết cuộc tranh luận, nhưng có lẽ sẽ không.

EDIT 3

Tôi có thể là một chút ám ảnh với bài viết này. Có lẽ bởi vì tôi cảm thấy rằng các nhà sản xuất Java đã vô tình truyền bá thông tin sai lệch. Nếu thay vì sử dụng từ "tham chiếu" cho con trỏ họ đã sử dụng một cái gì đó khác, hãy nói dingleberry, sẽ không có vấn đề gì. Bạn có thể nói, "Java vượt qua dingleberries theo giá trị chứ không phải bằng tham chiếu" và không ai có thể nhầm lẫn.

Đó là lý do chỉ các nhà phát triển Java có vấn đề với điều này. Họ nhìn vào từ "tham chiếu" và nghĩ rằng họ biết chính xác điều đó có nghĩa là gì, vì vậy họ thậm chí không bận tâm đến việc xem xét lập luận trái ngược.

Dù sao, tôi nhận thấy một bình luận trong một bài viết cũ hơn, nó đã tạo ra một sự tương tự bóng mà tôi thực sự thích. Đến nỗi tôi quyết định dán một số clip-art để tạo thành một bộ phim hoạt hình để minh họa cho luận điểm.

Truyền tham chiếu theo giá trị - Thay đổi đối với tham chiếu không được phản ánh trong phạm vi của trình gọi, nhưng thay đổi đối tượng là. Điều này là do tham chiếu được sao chép, nhưng cả bản gốc và bản sao đều tham chiếu đến cùng một đối tượng. Truyền tham chiếu đối tượng theo giá trị

Chuyển qua tham chiếu - Không có bản sao của tài liệu tham khảo. Tham chiếu đơn được chia sẻ bởi cả người gọi và chức năng được gọi. Mọi thay đổi đối với tham chiếu hoặc dữ liệu của Đối tượng được phản ánh trong phạm vi của người gọi. Đi qua tham khảo

CHỈNH SỬA 4

Tôi đã thấy các bài viết về chủ đề này mô tả việc triển khai tham số ở mức thấp trong Java, điều mà tôi nghĩ là tuyệt vời và rất hữu ích vì nó làm cho một ý tưởng trừu tượng cụ thể. Tuy nhiên, với tôi câu hỏi liên quan nhiều đến hành vi được mô tả trong đặc tả ngôn ngữ hơn là về việc thực hiện kỹ thuật của hành vi. Đây là một đoạn trích từ Đặc tả ngôn ngữ Java, phần 8.4.1 :

Khi phương thức hoặc hàm tạo được gọi (§15.12), các giá trị của biểu thức đối số thực tế khởi tạo các biến tham số mới được tạo, mỗi kiểu được khai báo, trước khi thực hiện phần thân của phương thức hoặc hàm tạo. Mã định danh xuất hiện trong DeclaratorId có thể được sử dụng như một tên đơn giản trong phần thân của phương thức hoặc hàm tạo để chỉ tham số chính thức.

Có nghĩa là, java tạo một bản sao của các tham số đã truyền trước khi thực hiện một phương thức. Giống như hầu hết những người đã nghiên cứu các trình biên dịch ở đại học, tôi đã sử dụng "The Dragon Book" đó là THE cuốn sách biên dịch. Nó có một mô tả hay về "Gọi theo giá trị" và "Gọi theo tham chiếu" trong Chương 1. Mô tả gọi theo giá trị khớp chính xác với Thông số kỹ thuật Java.

Quay trở lại khi tôi nghiên cứu các trình biên dịch - vào những năm 90, tôi đã sử dụng ấn bản đầu tiên của cuốn sách từ năm 1986 trước Java khoảng 9 hoặc 10 năm. Tuy nhiên, tôi chỉ chạy qua một bản sao của Phiên bản thứ 2 từ năm 2007 mà thực sự đề cập đến Java! Mục 1.6.6 được gắn nhãn "Cơ chế truyền tham số" mô tả tham số truyền khá độc đáo. Đây là một đoạn trích dưới tiêu đề "Gọi theo giá trị" có đề cập đến Java:

Trong cuộc gọi theo giá trị, tham số thực tế được ước tính (nếu đó là biểu thức) hoặc sao chép (nếu đó là biến). Giá trị được đặt ở vị trí thuộc về tham số chính thức tương ứng của thủ tục được gọi. Phương thức này được sử dụng trong C và Java và là một tùy chọn phổ biến trong C ++, cũng như trong hầu hết các ngôn ngữ khác.


4
Thành thật mà nói, bạn có thể đơn giản hóa câu trả lời này bằng cách nói Java chỉ truyền theo giá trị cho các kiểu nguyên thủy. Mọi thứ kế thừa từ Object đều được truyền tham chiếu một cách hiệu quả, trong đó tham chiếu là con trỏ bạn đi qua.
Scuba Steve

1
@JuanMendes, tôi chỉ sử dụng C ++ làm ví dụ; Tôi có thể cho bạn ví dụ trong các ngôn ngữ khác. Thuật ngữ "Truyền bằng tham chiếu" tồn tại rất lâu trước khi C ++ tồn tại. Đó là một thuật ngữ sách giáo khoa với một định nghĩa rất cụ thể. Và theo định nghĩa, Java không vượt qua được tham chiếu. Bạn có thể tiếp tục sử dụng thuật ngữ theo cách này nếu bạn muốn, nhưng cách sử dụng của bạn sẽ không phù hợp với định nghĩa của sách giáo khoa. Nó không chỉ là một con trỏ đến một con trỏ. Đây là một cấu trúc được cung cấp bởi ngôn ngữ để cho phép "Chuyển qua tham chiếu". Xin hãy xem kỹ ví dụ của tôi, nhưng làm ơn đừng hiểu ý tôi và tự mình tìm kiếm nó.
Sanjeev

2
@AutomatedMike Tôi nghĩ rằng việc mô tả Java là "tham chiếu qua" cũng gây hiểu nhầm, tham chiếu của họ không có gì hơn con trỏ. Như Sanjeev đã viết giá trị, tham chiếu và con trỏ là các thuật ngữ trong sách giáo khoa có ý nghĩa riêng của chúng bất kể người tạo Java sử dụng cái gì.
Jędrzej Dudkiewicz

1
@ArtanisZeratul điểm là tinh tế, nhưng không phức tạp. Xin hãy xem phim hoạt hình tôi đã đăng. Tôi cảm thấy nó khá đơn giản để làm theo. Java là Pass-by-value không chỉ là một ý kiến. Điều đó đúng theo định nghĩa của sách giáo khoa về giá trị truyền qua. Ngoài ra, "những người luôn nói rằng nó vượt qua giá trị" bao gồm các nhà khoa học máy tính như James Gosling, người tạo ra Java. Xin vui lòng xem các trích dẫn từ cuốn sách của mình dưới "Chỉnh sửa 2" trong bài viết của tôi.
Sanjeev

1
Thật là một câu trả lời tuyệt vời, "vượt qua tham chiếu" không tồn tại trong java (và các ngôn ngữ khác như JS).
newfolder

56

Theo tôi biết, Java chỉ biết gọi theo giá trị. Điều này có nghĩa là đối với các kiểu dữ liệu nguyên thủy, bạn sẽ làm việc với một bản sao và đối với các đối tượng bạn sẽ làm việc với một bản sao của tham chiếu đến các đối tượng. Tuy nhiên tôi nghĩ rằng có một số cạm bẫy; ví dụ, điều này sẽ không hoạt động:

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

Điều này sẽ điền vào Hello World chứ không phải World Hello vì trong chức năng trao đổi, bạn sử dụng các bản sao không ảnh hưởng đến các tham chiếu trong chính. Nhưng nếu đối tượng của bạn không bất biến, bạn có thể thay đổi nó chẳng hạn:

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

Điều này sẽ đưa Hello World vào dòng lệnh. Nếu bạn thay đổi StringBuffer thành String, nó sẽ tạo ra Hello vì String là bất biến. Ví dụ:

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

Tuy nhiên, bạn có thể tạo một trình bao bọc cho Chuỗi như thế này để có thể sử dụng nó với Chuỗi:

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

chỉnh sửa: tôi tin rằng đây cũng là lý do để sử dụng StringBuffer khi nói đến việc "thêm" hai Chuỗi vì bạn có thể sửa đổi đối tượng ban đầu mà bạn không thể với các đối tượng bất biến như String.


+1 cho thử nghiệm hoán đổi - có lẽ là cách đơn giản và dễ hiểu nhất để phân biệt giữa chuyển qua tham chiếuchuyển tham chiếu theo giá trị . Nếu bạn có thể dễ dàng viết một hàm swap(a, b)(1) hoán đổi abtừ POV của người gọi, (2) là không xác định kiểu đến mức gõ tĩnh cho phép (nghĩa là sử dụng nó với loại khác không đòi hỏi gì hơn là thay đổi các kiểu khai báo ab) và (3) không yêu cầu người gọi chuyển một cách rõ ràng một con trỏ hoặc tên, sau đó ngôn ngữ hỗ trợ chuyển qua tham chiếu.
cHao 17/12/13

"..cho các kiểu dữ liệu nguyên thủy, bạn sẽ làm việc với một bản sao và đối với các đối tượng bạn sẽ làm việc với một bản sao của tham chiếu đến các đối tượng" - được viết hoàn hảo!
Raúl

55

Không, nó không vượt qua được bằng cách tham khảo.

Java được truyền theo giá trị theo Đặc tả ngôn ngữ Java:

Khi phương thức hoặc hàm tạo được gọi (§15.12), các giá trị của biểu thức đối số thực tế khởi tạo các biến tham số mới được tạo , mỗi kiểu được khai báo, trước khi thực hiện phần thân của phương thức hoặc hàm tạo. Mã định danh xuất hiện trong DeclaratorId có thể được sử dụng như một tên đơn giản trong phần thân của phương thức hoặc hàm tạo để chỉ tham số chính thức .


52

Hãy để tôi cố gắng giải thích sự hiểu biết của tôi với sự giúp đỡ của bốn ví dụ. Java là pass-by-value và không phải là pass-by-Reference

/ **

Vượt qua giá trị

Trong Java, tất cả các tham số được truyền theo giá trị, tức là gán đối số phương thức không hiển thị cho người gọi.

* /

Ví dụ 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";
    }
}

Kết quả

output : output
value : Nikhil
valueflag : false

Ví dụ 2:

/ ** * * Truyền theo giá trị * * /

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

Kết quả

output : output
value : Nikhil
valueflag : false

Ví dụ 3:

/ ** Điều này 'Pass By Value có cảm giác' Pass By Reference '

Một số người nói các kiểu nguyên thủy và 'Chuỗi' là 'truyền theo giá trị' và các đối tượng là 'truyền theo tham chiếu'.

Nhưng từ ví dụ này, chúng ta có thể hiểu rằng nó chỉ còn nguyên vẹn thông qua giá trị, hãy nhớ rằng ở đây chúng ta đang chuyển tham chiếu làm giá trị. tức là: tham chiếu được truyền theo giá trị. Đó là lý do tại sao có thể thay đổi và nó vẫn đúng sau phạm vi địa phương. Nhưng chúng ta không thể thay đổi tham chiếu thực tế ngoài phạm vi ban đầu. điều đó có nghĩa là gì được thể hiện bằng ví dụ tiếp theo của 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";
    }
}

Kết quả

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

Ví dụ 4:

/ **

Ngoài những gì đã được đề cập trong Ví dụ3 (PassByValueObjectCase1.java), chúng tôi không thể thay đổi tham chiếu thực tế bên ngoài phạm vi ban đầu. "

Lưu ý: Tôi không dán mã cho private class Student. Định nghĩa lớp cho Studentgiống như Ví dụ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";
    }

}

Kết quả

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

49

Bạn không bao giờ có thể chuyển qua tham chiếu trong Java và một trong những cách rõ ràng là khi bạn muốn trả về nhiều hơn một giá trị từ một lệnh gọi phương thức. Hãy xem xét các đoạn mã sau trong 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;
}

Đôi khi bạn muốn sử dụng cùng một mẫu trong Java, nhưng bạn không thể; ít nhất là không trực tiếp. Thay vào đó bạn có thể làm một cái gì đó như thế này:

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

Như đã được giải thích trong các câu trả lời trước, trong Java, bạn chuyển một con trỏ tới mảng dưới dạng một giá trị getValues. Thế là đủ, bởi vì phương thức sau đó sửa đổi phần tử mảng và theo quy ước bạn mong muốn phần tử 0 chứa giá trị trả về. Rõ ràng bạn có thể làm điều này theo những cách khác, chẳng hạn như cấu trúc mã của bạn để điều này không cần thiết hoặc xây dựng một lớp có thể chứa giá trị trả về hoặc cho phép nó được đặt. Nhưng mẫu đơn giản có sẵn cho bạn trong C ++ ở trên không có sẵn trong Java.


48

Tôi nghĩ rằng tôi đã đóng góp câu trả lời này để thêm chi tiết từ Thông số kỹ thuật.

Đầu tiên, sự khác biệt giữa chuyển qua tham chiếu so với chuyển qua giá trị là gì?

Truyền bằng tham chiếu có nghĩa là tham số của các hàm được gọi sẽ giống như đối số được truyền của người gọi (không phải giá trị, mà là danh tính - chính biến).

Truyền theo giá trị có nghĩa là tham số của các hàm được gọi sẽ là một bản sao của đối số được truyền của người gọi.

Hoặc từ wikipedia, về chủ đề truyền qua

Trong đánh giá tham chiếu cuộc gọi (còn được gọi là tham chiếu qua), một hàm nhận được một tham chiếu ngầm đến một biến được sử dụng làm đối số, thay vì một bản sao của giá trị của nó. Điều này thường có nghĩa là hàm có thể sửa đổi (tức là gán cho) biến được sử dụng làm đối số, một thứ gì đó sẽ được người gọi nhìn thấy.

về chủ đề của giá trị

Trong cuộc gọi theo giá trị, biểu thức đối số được ước tính và giá trị kết quả được liên kết với biến tương ứng trong hàm [...]. Nếu chức năng hoặc thủ tục có thể gán giá trị cho các tham số của nó, chỉ có bản sao cục bộ của nó được gán [...].

Thứ hai, chúng ta cần biết Java sử dụng những gì trong các cách gọi phương thức của nó. Các ngôn ngữ Java Specification bang

Khi phương thức hoặc hàm tạo được gọi (§15.12), các giá trị của các biểu thức đối số thực tế khởi tạo các biến tham số mới được tạo , mỗi kiểu được khai báo, trước khi thực hiện phần thân của phương thức hoặc hàm tạo.

Vì vậy, nó gán (hoặc liên kết) giá trị của đối số cho biến tham số tương ứng.

Giá trị của đối số là gì?

Hãy xem xét các loại tham chiếu, trạng thái Đặc tả máy ảo Java

Có ba loại kiểu tham chiếu : kiểu lớp, kiểu mảng và kiểu giao diện. Các giá trị của chúng là các tham chiếu đến các thể hiện lớp, các mảng hoặc các thể hiện của lớp hoặc các mảng thực hiện các giao diện tương ứng.

Các ngôn ngữ Java Specification cũng nói

Các giá trị tham chiếu (thường chỉ là tham chiếu) là các con trỏ tới các đối tượng này và một tham chiếu null đặc biệt, tham chiếu đến không có đối tượng.

Giá trị của một đối số (của một số loại tham chiếu) là một con trỏ tới một đối tượng. Lưu ý rằng một biến, một lời gọi của một phương thức với kiểu trả về kiểu tham chiếu và một biểu thức tạo cá thể ( new ...) đều giải quyết một giá trị kiểu tham chiếu.

Vì thế

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

tất cả liên kết giá trị của một tham chiếu đến một Stringthể hiện với tham số mới được tạo của phương thức , param. Đây chính xác là những gì định nghĩa của pass-by-value mô tả. Như vậy, Java là giá trị truyền qua .

Việc bạn có thể theo tham chiếu để gọi một phương thức hoặc truy cập vào một trường của đối tượng được tham chiếu là hoàn toàn không liên quan đến cuộc hội thoại. Định nghĩa của tham chiếu qua là

Điều này thường có nghĩa là hàm có thể sửa đổi (tức là gán cho) biến được sử dụng làm đối số, một thứ gì đó sẽ được người gọi nhìn thấy.

Trong Java, sửa đổi biến có nghĩa là gán lại nó. Trong Java, nếu bạn gán lại biến trong phương thức, nó sẽ không được chú ý đến người gọi. Sửa đổi đối tượng được tham chiếu bởi biến là một khái niệm hoàn toàn khác.


Các giá trị nguyên thủy cũng được định nghĩa trong Đặc tả máy ảo Java, tại đây . Giá trị của loại là giá trị tích phân hoặc dấu phẩy động tương ứng, được mã hóa phù hợp (các bit 8, 16, 32, 64, v.v.).


42

Trong Java chỉ có các tham chiếu được truyền và được truyền theo giá trị:

Các đối số Java đều được truyền theo giá trị (tham chiếu được sao chép khi được sử dụng bởi phương thức):

Trong trường hợp các kiểu nguyên thủy, hành vi Java rất đơn giản: Giá trị được sao chép trong một thể hiện khác của kiểu nguyên thủy.

Trong trường hợp của Đối tượng, điều này giống nhau: Biến đối tượng là con trỏ (xô) chỉ giữ địa chỉ của Đối tượng được tạo bằng từ khóa "mới" và được sao chép như các kiểu nguyên thủy.

Hành vi có thể xuất hiện khác với các kiểu nguyên thủy: Bởi vì biến đối tượng được sao chép chứa cùng một địa chỉ (cho cùng một Đối tượng). Nội dung / thành viên của đối tượng vẫn có thể được sửa đổi trong một phương thức và sau đó truy cập bên ngoài, tạo ảo giác rằng chính đối tượng (có chứa) được truyền bằng tham chiếu.

Các đối tượng "Chuỗi" dường như là một ví dụ điển hình cho truyền thuyết đô thị nói rằng "Các đối tượng được truyền bằng tham chiếu":

Trong thực tế, bằng cách sử dụng một phương thức, bạn sẽ không bao giờ có thể, để cập nhật giá trị của Chuỗi được truyền dưới dạng đối số:

Một đối tượng chuỗi, giữ các ký tự bởi một mảng được khai báo cuối cùng không thể sửa đổi. Chỉ địa chỉ của Đối tượng có thể được thay thế bằng địa chỉ khác bằng cách sử dụng "mới". Sử dụng "mới" để cập nhật biến, sẽ không cho phép Đối tượng được truy cập từ bên ngoài, vì biến ban đầu được truyền bởi giá trị và được sao chép.


Vì vậy, đó là byRef liên quan đến các đối tượng và byVal liên quan đến nguyên thủy?
Mox

@mox vui lòng đọc: Các đối tượng không được chuyển qua tham chiếu, đây là một ledgend: String a = new String ("không thay đổi");
dùng1767316

1
@Aaron "Truyền bằng tham chiếu" không có nghĩa là "truyền giá trị là thành viên của loại được gọi là 'tham chiếu' trong Java". Hai cách sử dụng "tham chiếu" có nghĩa là những thứ khác nhau.
philipxy

2
Câu trả lời khá khó hiểu. Lý do tại sao bạn không thể thay đổi một đối tượng chuỗi trong một phương thức, được truyền qua tham chiếu, là vì các đối tượng Chuỗi không thay đổi theo thiết kế và bạn không thể làm những việc như thế strParam.setChar ( i, newValue ). Điều đó nói rằng, các chuỗi, như mọi thứ khác, được truyền bằng giá trị và, vì String là loại không nguyên thủy, giá trị đó là tham chiếu đến thứ được tạo bằng mới và bạn có thể kiểm tra điều này bằng cách sử dụng String.i INTERN () .
zakmck

1
Thay vào đó, Bạn không thể thay đổi một chuỗi thông qua param = "chuỗi khác" (tương đương với Chuỗi mới ("chuỗi khác")) vì giá trị tham chiếu của param (hiện chỉ đến "chuỗi khác") không thể quay lại từ phương thức thân hình. Nhưng điều đó đúng với bất kỳ đối tượng nào khác, sự khác biệt là, khi giao diện lớp cho phép, bạn có thể thực hiện param.changeMe () và đối tượng cấp cao nhất được giới thiệu sẽ thay đổi vì param đang chỉ vào nó, mặc dù chính giá trị tham chiếu của param (vị trí địa chỉ, theo thuật ngữ C) không thể bật lên từ phương thức.
zakmck

39

Sự khác biệt, hoặc có lẽ chỉ là cách tôi nhớ như tôi đã từng có cùng một ấn tượng như người đăng ban đầu là thế này: Java luôn vượt qua giá trị. Tất cả các đối tượng (trong Java, bất cứ thứ gì ngoại trừ nguyên thủy) trong Java đều là các tham chiếu. Các tài liệu tham khảo được thông qua bởi giá trị.


2
Tôi thấy câu thứ hai đến cuối cùng của bạn rất sai lệch. Điều đó không đúng khi "tất cả các đối tượng trong Java là tài liệu tham khảo". Nó chỉ là các tham chiếu đến các đối tượng được tham chiếu.
Dawood ibn Kareem

37

Như nhiều người đã đề cập trước đây, Java luôn là giá trị truyền qua

Dưới đây là một ví dụ khác sẽ giúp bạn hiểu sự khác biệt ( ví dụ hoán đổi cổ điển ):

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

Bản in:

Trước: a = 2, b = 3
Sau: a = 2, b = 3

Điều này xảy ra bởi vì iA và iB là các biến tham chiếu cục bộ mới có cùng giá trị của các tham chiếu đã truyền (chúng tương ứng với a và b). Vì vậy, cố gắng thay đổi các tham chiếu của iA hoặc iB sẽ chỉ thay đổi trong phạm vi cục bộ chứ không nằm ngoài phương thức này.


32

Java chỉ vượt qua theo giá trị. Một ví dụ rất đơn giản để xác nhận điều này.

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
Đây là cách rõ ràng nhất, đơn giản nhất để thấy rằng Java vượt qua giá trị. Giá trị của obj( null) đã được chuyển đến init, không tham chiếu đến obj.
David Schwartz

31

Tôi luôn nghĩ về nó như là "vượt qua bản sao". Nó là một bản sao của giá trị có thể là nguyên thủy hoặc tham chiếu. Nếu nó là một nguyên thủy, nó là một bản sao của các bit là giá trị và nếu nó là một Object thì nó là một bản sao của tham chiếu.

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

đầu ra của java PassByCopy:

tên = tên Maxx
= Fido

Các lớp và chuỗi trình bao bọc nguyên thủy là bất biến nên mọi ví dụ sử dụng các kiểu đó sẽ không hoạt động giống như các kiểu / đối tượng khác.


5
"vượt qua bằng cách sao chép" là những gì pass-by-value phương tiện .
TJ Crowder

@TJCung cấp. "Truyền qua bản sao" có thể là một cách thích hợp hơn để diễn đạt cùng một khái niệm cho các mục đích Java: bởi vì về mặt ngữ nghĩa, nó rất khó để đánh giày trong một ý tưởng gần giống với việc chuyển qua tham chiếu, đó là điều bạn muốn trong Java.
mike gặm nhấm

@mikerodent - Xin lỗi, tôi đã không làm theo. :-) "Pass-by-value" và "pass-by-Reference" là các thuật ngữ chính xác cho các khái niệm này. Chúng ta nên sử dụng chúng (cung cấp định nghĩa của chúng khi mọi người cần chúng) hơn là tạo ra những cái mới. Ở trên tôi đã nói "pass by copy" nghĩa là "pass-by-value" nghĩa là gì, nhưng thực ra, không rõ "pass by copy" nghĩa là gì - một bản sao của cái gì? Giá trị? (Ví dụ, tham chiếu đối tượng.) Bản thân đối tượng? Vì vậy, tôi gắn bó với các điều khoản của nghệ thuật. :-)
TJ Crowder

28

Không giống như một số ngôn ngữ khác, Java không cho phép bạn lựa chọn giữa pass-by-value và pass-by-Reference, tất cả các đối số được truyền theo giá trị. Một cuộc gọi phương thức có thể chuyển hai loại giá trị cho một phương thức các bản sao của các giá trị nguyên thủy (ví dụ: giá trị của int và double) và các bản sao của các tham chiếu đến các đối tượng.

Khi một phương thức sửa đổi một tham số kiểu nguyên thủy, các thay đổi đối với tham số không có hiệu lực đối với giá trị đối số ban đầu trong phương thức gọi.

Khi nói đến các đối tượng, bản thân các đối tượng không thể được truyền cho các phương thức. Vì vậy, chúng tôi vượt qua tham chiếu (địa chỉ) của đối tượng. Chúng ta có thể thao tác đối tượng ban đầu bằng cách sử dụng tham chiếu này.

Cách Java tạo và lưu trữ các đối tượng: Khi chúng ta tạo một đối tượng, chúng ta lưu trữ địa chỉ của đối tượng trong một biến tham chiếu. Hãy phân tích các tuyên bố sau.

Account account1 = new Account();

Tài khoản của tài khoản11 là loại và tên của biến tham chiếu, trực tiếp là nhà điều hành chuyển nhượng, mới có thể yêu cầu số lượng không gian cần thiết từ hệ thống. Hàm tạo bên phải của từ khóa mới tạo đối tượng được gọi ngầm bởi từ khóa mới. Địa chỉ của đối tượng được tạo (kết quả của giá trị bên phải, là một biểu thức gọi là "biểu thức tạo cá thể lớp") được gán cho giá trị bên trái (là biến tham chiếu có tên và loại được chỉ định) bằng cách sử dụng toán tử gán.

Mặc dù tham chiếu của một đối tượng được truyền theo giá trị, một phương thức vẫn có thể tương tác với đối tượng được tham chiếu bằng cách gọi các phương thức công khai của nó bằng cách sử dụng bản sao của tham chiếu của đối tượng. Vì tham chiếu được lưu trong tham số là bản sao của tham chiếu được truyền dưới dạng đối số, tham số trong phương thức được gọi và đối số trong phương thức gọi tham chiếu đến cùng một đối tượng trong bộ nhớ.

Truyền tham chiếu đến mảng, thay vì chính các đối tượng mảng, có ý nghĩa vì lý do hiệu năng. Vì mọi thứ trong Java đều được truyền theo giá trị, nếu các đối tượng mảng được truyền, một bản sao của từng phần tử sẽ được truyền. Đối với các mảng lớn, điều này sẽ lãng phí thời gian và tiêu thụ dung lượng đáng kể cho các bản sao của các phần tử.

Trong hình ảnh bên dưới, bạn có thể thấy chúng ta có hai biến tham chiếu (Chúng được gọi là con trỏ trong C / C ++ và tôi nghĩ thuật ngữ đó giúp dễ hiểu tính năng này hơn.) Trong phương thức chính. Các biến nguyên thủy và tham chiếu được giữ trong bộ nhớ ngăn xếp (bên trái trong hình ảnh bên dưới). biến tham chiếu mảng1 và mảng2 "điểm" (như các lập trình viên C / C ++ gọi nó) hoặc tham chiếu đến mảng a và b tương ứng, là các đối tượng (giá trị mà các biến tham chiếu này giữ là địa chỉ của các đối tượng) trong bộ nhớ heap (bên phải trong hình ảnh bên dưới) .

Vượt qua ví dụ giá trị 1

Nếu chúng ta truyền giá trị của biến tham chiếu mảng1 làm đối số cho phương thức ReverseArray, một biến tham chiếu được tạo trong phương thức và biến tham chiếu đó bắt đầu trỏ đến cùng một mảng (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);
    }
}

Vượt qua ví dụ giá trị 2

Vì vậy, nếu chúng ta nói

array1[0] = 5;

trong phương thức ReverseArray, nó sẽ tạo ra một sự thay đổi trong mảng a.

Chúng ta có một biến tham chiếu khác trong phương thức ReverseArray (mảng2) trỏ đến một mảng c. Nếu chúng ta muốn nói

array1 = array2;

trong phương thức ReverseArray, sau đó biến tham chiếu mảng1 trong phương thức ReverseArray sẽ dừng trỏ đến mảng a và bắt đầu trỏ đến mảng c (Đường chấm trong hình ảnh thứ hai).

Nếu chúng ta trả về giá trị của biến tham chiếu mảng2 là giá trị trả về của phương thức ReverseArray và gán giá trị này cho biến tham chiếu mảng1 trong phương thức chính, mảng1 trong main sẽ bắt đầu trỏ đến mảng c.

Vì vậy, hãy viết tất cả những điều chúng ta đã làm ngay bây giờ.

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 */
    }
}

nhập mô tả hình ảnh ở đây

Và bây giờ phương thức ReverseArray đã kết thúc, các biến tham chiếu của nó (mảng1 và mảng2) đã biến mất. Điều đó có nghĩa là bây giờ chúng ta chỉ có hai biến tham chiếu trong mảng phương thức chính mảng 1 và mảng2, lần lượt trỏ đến mảng c và b. Không có biến tham chiếu nào được trỏ đến đối tượng (mảng) a. Vì vậy, nó là đủ điều kiện để thu gom rác.

Bạn cũng có thể gán giá trị của mảng2 trong chính cho mảng1. mảng1 sẽ bắt đầu trỏ đến b.


27

Tôi đã tạo ra một chủ đề dành cho các loại câu hỏi cho bất kỳ ngôn ngữ lập trình nào ở đây .

Java cũng được đề cập . Dưới đây là tóm tắt ngắn gọn:

  • Java truyền cho nó các tham số theo giá trị
  • "theo giá trị" là cách duy nhất trong java để truyền tham số cho phương thức
  • sử dụng các phương thức từ đối tượng được cung cấp làm tham số sẽ thay đổi đối tượng làm tham chiếu trỏ đến các đối tượng ban đầu. (nếu phương thức đó tự thay đổi một số giá trị)

27

Để làm cho một câu chuyện dài ngắn, các đối tượng Java có một số thuộc tính rất đặc biệt.

Nói chung, Java có các kiểu dữ ( int, bool, char, double, vv) được chuyển trực tiếp theo giá trị. Sau đó, Java có các đối tượng (mọi thứ xuất phát từ java.lang.Object). Các đối tượng thực sự luôn được xử lý thông qua một tham chiếu (một tham chiếu là một con trỏ mà bạn không thể chạm vào). Điều đó có nghĩa là trong thực tế, các đối tượng được truyền bằng tham chiếu, vì các tham chiếu thường không thú vị. Tuy nhiên, điều đó có nghĩa là bạn không thể thay đổi đối tượng nào được trỏ đến khi chính tham chiếu được truyền theo giá trị.

Điều này nghe có vẻ lạ và khó hiểu? Hãy xem xét cách C thực hiện chuyển qua tham chiếu và truyền theo giá trị. Trong C, quy ước mặc định được truyền theo giá trị. void foo(int x)vượt qua một int theo giá trị. void foo(int *x)là một hàm không muốn một int a, nhưng một con trỏ tới int : foo(&a). Người ta sẽ sử dụng điều này với &toán tử để truyền một địa chỉ biến.

Mang nó đến C ++ và chúng tôi có tài liệu tham khảo. Các tham chiếu về cơ bản (trong ngữ cảnh này) đường cú pháp ẩn phần con trỏ của phương trình: void foo(int &x)được gọi bởi foo(a), trong đó chính trình biên dịch biết rằng đó là một tham chiếu và địa chỉ của tham chiếu akhông được thông qua. Trong Java, tất cả các biến tham chiếu đến các đối tượng thực sự thuộc kiểu tham chiếu, thực tế là buộc gọi theo tham chiếu cho hầu hết các mục đích và mục đích mà không có sự kiểm soát chi tiết (và độ phức tạp) có sẵn, ví dụ, C ++.


Tôi nghĩ rằng nó rất gần với sự hiểu biết của tôi về đối tượng Java và tài liệu tham khảo của nó. Đối tượng trong Java được truyền cho một phương thức bởi một bản sao tham chiếu (hoặc bí danh).
MaxZoom
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.