Czy Java jest „pass-by-reference” czy „pass-by-value”?


6577

Zawsze myślałem, że Java jest przekazywana przez odniesienie .

Widziałem jednak kilka postów na blogu (na przykład ten blog ), które twierdzą, że tak nie jest.

Nie sądzę, że rozumiem ich rozróżnienie.

Jakie jest wyjaśnienie?


705
Uważam, że wiele zamieszania w tej kwestii wiąże się z faktem, że różni ludzie mają różne definicje terminu „odniesienie”. Ludzie pochodzący ze środowiska C ++ zakładają, że „referencja” musi oznaczać, co to znaczy w C ++, ludzie z tła C zakładają, że „referencja” musi być taka sama jak „wskaźnik” w ich języku i tak dalej. To, czy słusznie jest powiedzieć, że Java przechodzi przez referencję, naprawdę zależy od tego, co rozumie się przez „referencję”.
Grawitacja

119
Staram się konsekwentnie używać terminologii zawartej w artykule na temat strategii oceny . Należy zauważyć, że chociaż artykuł wskazuje, że terminy różnią się znacznie w zależności od społeczności, podkreśla, że ​​semantyka call-by-valuei call-by-referenceróżni się w bardzo istotny sposób . (Osobiście wolę korzystać z call-by-object-sharingtych dni call-by-value[-of-the-reference], ponieważ to opisuje semantykę na wysokim poziomie i nie powoduje konfliktu call-by-value, co jest podstawową implementacją.)

59
@Gravity: Czy możesz wstawić swój komentarz na OGROMNYM billboardzie lub czymś takim? To cały problem w pigułce. I pokazuje, że cała ta sprawa jest semantyką. Jeśli nie uzgodnimy podstawowej definicji odwołania, nie uzgodnimy odpowiedzi na to pytanie :)
MadConan,

29
Myślę, że zamieszanie polega na „przekazywaniu przez odniesienie” w porównaniu z „semantyką odniesienia”. Java jest pass-by-value z semantyką odniesienia.
spraff

11
@ Grawitacja, podczas gdy masz całkowitą rację, że ludzie pochodzący z C ++ instynktownie mają inną intuicję dotyczącą terminu „referencja”, osobiście uważam, że ciało jest bardziej pochowane w „przez”. „Przekaż” jest mylące, ponieważ absolutnie różni się od „Przekazywania” w Javie. W C ++ jednak tak nie jest. W C ++ możesz powiedzieć „przekazanie referencji” i zrozumiałe jest, że przejdzie swap(x,y)test .

Odpowiedzi:


5838

Java jest zawsze wartością przekazywaną . Niestety, gdy przekazujemy wartość obiektu, przekazujemy referencję do niego. Jest to mylące dla początkujących.

Wygląda to tak:

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
}

W powyższym przykładzie aDog.getName()nadal powróci "Max". Wartość aDogciągu mainnie zmienia się w funkcji fooz Dog "Fifi"jak odwołanie do obiektu jest przekazywane przez wartość. Gdyby były przekazywane przez referencję, a następnie aDog.getName()w mainwrócą "Fifi"po wywołaniu foo.

Również:

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

W powyższym przykładzie Fifijest to imię psa po wywołaniu, foo(aDog)ponieważ nazwa obiektu została ustawiona wewnątrz foo(...). Wszelkie operacje, które foowykonuje, dsą takie, że dla wszystkich praktycznych celów są one wykonywane aDog, ale nie można zmienić wartości aDogsamej zmiennej .


263
Czy to nie myli problemu z wewnętrznymi szczegółami? Nie ma różnicy pojęciowej między „przekazywaniem referencji” a „przekazywaniem wartości referencji”, zakładając, że masz na myśli „wartość wewnętrznego wskaźnika do obiektu”.
izb

383
Ale jest subtelna różnica. Spójrz na pierwszy przykład. Gdyby było to wyłącznie przekazane przez odniesienie, nazwaDog.name to „Fifi”. Nie jest - referencja, którą otrzymujesz, jest referencją wartości, która jeśli zostanie zastąpiona, zostanie przywrócona po wyjściu z funkcji.
erlando

300
@Lorenzo: Nie, w Javie wszystko jest przekazywane wartością. Prymitywy są przekazywane przez wartość, a odwołania do obiektów są przekazywane przez wartość. Same obiekty nigdy nie są przekazywane do metody, ale obiekty są zawsze w stercie, a do metody jest przekazywane tylko odwołanie do obiektu.
Esko Luontola

294
Moja próba dobrego sposobu wizualizacji przechodzącego obiektu: Wyobraź sobie balon. Wywołanie fxn jest jak przywiązanie drugiego sznurka do balonu i przekazanie linii do fxn. parametr = new Balloon () wycina ten ciąg i utworzy nowy balon (ale nie ma to wpływu na oryginalny balon). parametr.pop () nadal go wyskakuje, ponieważ podąża za ciągiem do tego samego, oryginalnego balonu. Java jest przekazywana przez wartość, ale przekazywana wartość nie jest głęboka, jest na najwyższym poziomie, tj. Prymitywny lub wskaźnik. Nie należy tego mylić z głębokim przekazywaniem wartości, w której obiekt jest całkowicie klonowany i przekazywany.
dhackner

150
Mylące jest to, że odwołania do obiektów są w rzeczywistości wskaźnikami. Na początku SUN nazwał je wskaźnikami. Następnie marketing poinformował, że „wskaźnik” to złe słowo. Ale nadal widzisz „poprawną” nomenklaturę w NullPointerException.
Umowa prof. Falkena została naruszona

3035

Właśnie zauważyłem, że odwoływałeś się do mojego artykułu .

Specyfikacja Java mówi, że wszystko w Javie jest przekazywane według wartości. W Javie nie ma czegoś takiego jak „pass-by-reference”.

Kluczem do zrozumienia tego jest coś takiego

Dog myDog;

to nie pies; to właściwie wskaźnik do psa.

Oznacza to, że masz

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

zasadniczo przekazujesz adres utworzonego Dogobiektu do foometody

(Mówię zasadniczo, ponieważ wskaźniki Java nie są adresami bezpośrednimi, ale najłatwiej o nich myśleć w ten sposób)

Załóżmy, że Dogobiekt znajduje się pod adresem pamięci 42. Oznacza to, że przekazujemy 42 metodę.

jeśli metoda została zdefiniowana jako

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

spójrzmy na to, co się dzieje.

  • parametr someDogjest ustawiony na wartość 42
  • na linii „AAA”
    • someDogpodąża za Dognim wskazuje ( Dogobiekt pod adresem 42)
    • że Dog(ten pod adresem 42) jest proszony o zmianę nazwiska na Max
  • na linii „BBB”
    • nowy Dogjest tworzony. Powiedzmy, że ma adres 74
    • przypisujemy parametr someDogdo 74
  • na linii „CCC”
    • someDog jest śledzony do wskazanego przez Dogniego Dogobiektu ( obiekt pod adresem 74)
    • że Dog(ten pod adresem 74) jest proszony o zmianę nazwiska na Rowlf
  • wtedy wracamy

Zastanówmy się teraz, co dzieje się poza metodą:

Zmieniło się myDog?

Jest klucz.

Pamiętając, że myDogjest to wskaźnik , a nie Dogfakt, odpowiedź brzmi NIE. myDognadal ma wartość 42; nadal wskazuje na oryginał Dog(należy jednak pamiętać, że z powodu wiersza „AAA” jego nazwa to teraz „Max” - wciąż ten sam pies; myDogwartość nie uległa zmianie).

Podążanie za adresem i zmiana tego, co znajduje się na jego końcu , są całkowicie poprawne ; nie zmienia to jednak zmiennej.

Java działa dokładnie tak jak C. Możesz przypisać wskaźnik, przekazać wskaźnik do metody, podążać za wskaźnikiem w metodzie i zmienić wskazane dane. Nie można jednak zmienić miejsca wskazanego przez ten wskaźnik.

W C ++, Ada, Pascal i innych językach, które obsługują przekazywanie przez referencję, możesz faktycznie zmienić przekazywaną zmienną.

Gdyby Java miała semantykę przekazywania przez odniesienie, foometoda, którą zdefiniowaliśmy powyżej zmieniłaby, gdzie myDogwskazywałby, gdy przypisano ją someDogw linii BBB.

Pomyśl o parametrach odniesienia jako o aliasach przekazywanej zmiennej. Kiedy ten alias jest przypisany, podobnie jak zmienna, która została przekazana.


164
Właśnie dlatego powszechny refren „Java nie ma wskaźników” jest tak mylący.
Beska,

134
Mylisz się, imho. „Pamiętając, że myDog jest wskaźnikiem, a nie prawdziwym psem, odpowiedź brzmi NIE. MyDog nadal ma wartość 42; nadal wskazuje na oryginalnego psa”. myDog ma wartość 42, ale argument nazwy zawiera teraz „Max” zamiast „Rover” w linii // AAA.
Özgür,

180
Pomyśl o tym w ten sposób. Ktoś ma adres Ann Arbor, MI (moje miasto rodzinne, GO BLUE!) Na kartce papieru o nazwie „annArborLocation”. Kopiujesz go na kartce papieru o nazwie „myDestination”. Możesz jechać do „myDestination” i posadzić drzewo. Być może coś zmieniłeś w mieście w tym miejscu, ale to nie zmienia LAT / LON, który został napisany na żadnym papierze. Możesz zmienić LAT / LON na „myDestination”, ale to nie zmienia „annArborLocation”. To pomaga?
Scott Stanchfield,

43
@ Scott Stanchfield: Przeczytałem twój artykuł około roku temu i naprawdę pomogło mi to wyjaśnić. Dzięki! Pozwólcie, że pokornie zasugeruję mały dodatek: należy wspomnieć, że w rzeczywistości istnieje konkretny termin opisujący tę formę „wywołania według wartości, gdzie wartość jest referencją”, która została wymyślona przez Barbarę Liskov do opisu strategii oceny jej języka CLU w 1974, aby uniknąć nieporozumień, takich jak ten, do którego odnosi się artykuł: zadzwoń przez udostępnianie (czasem nazywane wywołanie przez udostępnianie obiektów lub po prostu wywołanie przez obiekt ), co w zasadzie doskonale opisuje semantykę.
Jörg W Mittag,

29
@Gevorg - Języki C / C ++ nie posiadają koncepcji „wskaźnika”. Istnieją inne języki, które używają wskaźników, ale nie zezwalają na te same typy manipulacji wskaźnikami, na jakie pozwalają C / C ++. Java ma wskaźniki; są po prostu chronieni przed psotami.
Scott Stanchfield

1740

Java zawsze przekazuje argumenty według wartości , NIE przez referencję.


Pozwól mi wyjaśnić to na przykładzie :

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

}

Wyjaśnię to krokami:

  1. Deklarowanie odwołania o nazwie ftype Fooi przypisanie mu nowego obiektu typu Fooz atrybutem "f".

    Foo f = new Foo("f");

    wprowadź opis zdjęcia tutaj

  2. Od strony metody deklarowane jest odwołanie typu Fooo nazwie ai początkowo przypisane null.

    public static void changeReference(Foo a)

    wprowadź opis zdjęcia tutaj

  3. Podczas wywoływania metody changeReferencedo odwołania azostanie przypisany obiekt przekazywany jako argument.

    changeReference(f);

    wprowadź opis zdjęcia tutaj

  4. Deklarowanie odwołania o nazwie btype Fooi przypisanie mu nowego obiektu typu Fooz atrybutem "b".

    Foo b = new Foo("b");

    wprowadź opis zdjęcia tutaj

  5. a = bdokonuje nowego przypisania do odwołania a, a nie f obiektu, którego atrybutem jest "b".

    wprowadź opis zdjęcia tutaj

  6. Podczas wywoływania modifyReference(Foo c)metody ctworzone jest odwołanie i przypisywany obiektowi z atrybutem "f".

    wprowadź opis zdjęcia tutaj

  7. c.setAttribute("c");zmieni atrybut obiektu, który cwskazuje na niego, i jest to ten sam obiekt, który fwskazuje na to.

    wprowadź opis zdjęcia tutaj

Mam nadzieję, że rozumiesz teraz, jak przekazywanie obiektów jako argumentów działa w Javie :)


82
+1 fajne rzeczy. dobre diagramy. Znalazłem też fajną zwięzłą stronę tutaj adp-gmbh.ch/php/pass_by_reference.html OK Przyznaję, że jest napisany w PHP, ale jest to podstawa zrozumienia różnicy, która moim zdaniem jest ważna (i jak manipulować tą różnicą, aby Twoje potrzeby).
DaveM

5
@ Eng.Fouad To miłe wytłumaczenie, ale jeśli awskazuje na ten sam obiekt jako f(i nigdy nie dostaje własnej kopii obiektu na to fwskazuje), wszelkie zmiany w obiekcie dokonanym za pomocą apowinny frównież się zmodyfikować (ponieważ oba działają z tym samym obiektem ), więc w pewnym momencie amusi uzyskać własną kopięf punktów obiektu .
0x6C38

14
@MrD, gdy „a” wskazuje na ten sam obiekt „f”, wskazuje również, że wszelkie zmiany dokonane w tym obiekcie za pomocą „a” można również zaobserwować za pomocą „f”, ALE TO NIE ZMIENIŁO „f”. „f” nadal wskazuje na ten sam obiekt. Możesz całkowicie zmienić obiekt, ale nigdy nie możesz zmienić tego, na co wskazuje „f”. Jest to podstawowa kwestia, której z jakiegoś powodu niektórzy ludzie po prostu nie rozumieją.
Mike Braun,

@MikeBraun ... co? Teraz mnie pomyliłeś: S. Czy to, co właśnie napisałeś, nie jest sprzeczne z tym, co pokazuje 6. i 7.?
Zła pralka,

6
To najlepsze wytłumaczenie, jakie znalazłem. Nawiasem mówiąc, co z podstawową sytuacją? Na przykład argument wymaga typu int, czy nadal przekazuje kopię zmiennej int do argumentu?
allenwang

732

To da ci wgląd w to, jak naprawdę działa Java, do tego stopnia, że ​​w następnej dyskusji na temat przekazywania Java przez odniesienie lub przekazywanie wartości po prostu się uśmiechniesz :-)

Krok pierwszy, proszę, wymaż z pamięci to słowo, które zaczyna się od „p” „_ _ _ _ _ _ _”, szczególnie jeśli pochodzisz z innych języków programowania. Java i „p” nie mogą być zapisane w tej samej książce, na forum, a nawet w txt.

Krok drugi pamiętaj, że kiedy przekazujesz obiekt do metody, przekazujesz odwołanie do obiektu, a nie sam obiekt.

  • Student : Mistrzyni, czy to oznacza, że ​​Java jest przekazywana przez odniesienie?
  • Mistrz : Konik polny, nr

Pomyśl teraz o tym, czym jest odwołanie / zmienna obiektu:

  1. Zmienna przechowuje bity, które mówią JVM, jak dostać się do odnośnego obiektu w pamięci (sterty).
  2. Podczas przekazywania argumentów do metody NIE przekazujesz zmiennej referencyjnej, ale kopię bitów w zmiennej referencyjnej . Coś takiego: 3bad086a. 3bad086a reprezentuje sposób dotarcia do przekazywanego obiektu.
  3. Właśnie przekazujesz 3bad086a, że ​​jest to wartość referencji.
  4. Podajesz wartość referencji, a nie samą referencję (a nie obiekt).
  5. Ta wartość jest faktycznie KOPIOWANA i przekazana do metody .

Poniżej (nie próbuj kompilować / wykonać tego ...):

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

Co się dzieje?

  • Zmienna osoba jest tworzona w linii nr 1 i na początku ma wartość null.
  • Nowy obiekt osoby jest tworzony w linii nr 2, przechowywany w pamięci, a zmienna osoba otrzymuje odniesienie do obiektu osoby. To jest jego adres. Powiedzmy, że 3bad086a.
  • Zmienna osoba posiadająca adres obiektu jest przekazywana do funkcji w linii nr 3.
  • W linii nr 4 można usłyszeć dźwięk ciszy
  • Sprawdź komentarz do wiersza # 5
  • Lokalna zmienna metody - anotherReferenceToTheSamePersonObject - jest tworzona, a następnie pojawia się magia w linii nr 6:
    • Zmienna / osoba odniesienia jest kopiowana krok po kroku i przekazywana do anotherReferenceToTheSamePersonObject wewnątrz funkcji.
    • Nie są tworzone nowe wystąpienia Person.
    • Zarówno „ person ”, jak i „ anotherReferenceToTheSamePersonObject ” mają tę samą wartość 3bad086a.
    • Nie próbuj tego, ale osoba == anotherReferenceToTheSamePersonObject byłaby prawdą.
    • Obie zmienne mają IDENTYCZNE KOPIE odniesienia i oba odnoszą się do tego samego obiektu osoby, SAMEGO obiektu na stosie, a NIE KOPII.

Obraz jest wart tysiąca słów:

Przekaż według wartości

Zauważ, że strzałki anotherReferenceToTheSamePersonObject są skierowane w stronę obiektu, a nie w kierunku zmiennej osoby!

Jeśli go nie dostałeś, zaufaj mi i pamiętaj, że lepiej jest powiedzieć, że Java jest wartością przekazywaną . Cóż, podaj wartość odniesienia . No cóż, jeszcze lepiej jest przekazać wartość zmiennej! ;)

Teraz możesz mnie nienawidzić, ale zauważ, że biorąc pod uwagę to, nie ma różnicy między przekazywaniem prymitywnych typów danych a obiektami, gdy mówimy o argumentach metod.

Zawsze przekazujesz kopię bitów wartości referencji!

  • Jeśli jest to pierwotny typ danych, te bity będą zawierać wartość samego pierwotnego typu danych.
  • Jeśli jest to Obiekt, bity będą zawierać wartość adresu, który mówi JVM, jak dostać się do Obiektu.

Java jest przekazywana przez wartość, ponieważ w metodzie można modyfikować obiekt, do którego istnieje odwołanie, ile chcesz, ale bez względu na to, jak bardzo się starasz, nigdy nie będziesz w stanie zmodyfikować przekazywanej zmiennej, która będzie nadal odwoływać się (nie p _ _ _ _ _ _ _) ten sam obiekt bez względu na wszystko!


Funkcja changeName powyżej nigdy nie będzie w stanie zmodyfikować rzeczywistej treści (wartości bitów) przekazanego odwołania. Innymi słowy changeName nie może sprawić, że Osoba odniesie się do innego obiektu.


Oczywiście możesz to skrócić i powiedzieć, że Java jest wartością dodaną!


5
Czy masz na myśli wskaźniki? .. Jeśli poprawnie to zrozumiem public void foo(Car car){ ... }, caroznacza to , że jest lokalny fooi zawiera lokalizację stosu obiektu? Więc jeśli zmienię carwartość car = new Car(), wskaże inny obiekt na stercie? a jeśli zmienię wartość carwłaściwości o car.Color = "Red", Obiekt we wskazanej stercie carzostanie zmodyfikowany. Czy to samo w C #? Proszę odpowiedz! Dzięki!
dpp

11
@domanokz Zabijasz mnie, proszę nie powtarzaj tego słowa! ;) Zauważ, że mogłem odpowiedzieć na to pytanie, nie mówiąc też „odniesienia”. To kwestia terminologii, a „p” jeszcze gorzej. Niestety ja i Szkot mamy różne poglądy na ten temat. Myślę, że wiesz, jak to działa w Javie, teraz możesz nazwać to przekazywanie według wartości, udostępnianie obiektów, kopiowanie wartości zmiennej lub możesz wymyślić coś innego! Nie obchodzi mnie to tak długo, jak wiesz, jak to działa i co zawiera zmienna typu Object: tylko adres skrzynki pocztowej! ;)
Marsellus Wallace

9
Wygląda na to, że właśnie przekazałeś referencję ? Mam zamiar poprzeć fakt, że Java jest nadal kopiowanym językiem pass-by-reference. Fakt, że jest to skopiowane odwołanie, nie zmienia terminologii. Oba REFERENCJE nadal wskazują na ten sam obiekt. To argument
purysty

3
Wpadnij więc na teoretyczną linię nr 9, System.out.println(person.getName());co się pojawi? „Tom” czy „Jerry”? To ostatnia rzecz, która pomoże mi pokonać to zamieszanie.
TheBrenny,

1
„Wartość referencyjna ” ostatecznie mnie wyjaśniła.
Alexander Shubert

689

Java jest zawsze przekazywana przez wartość, bez wyjątków, zawsze .

Jak to możliwe, że ktokolwiek może być w ten sposób zdezorientowany i wierzyć, że Java jest przekazywana przez referencję, lub myśli, że ma przykład, że Java działa jako przekazywana przez referencję? Kluczową kwestią jest to, że Java w żadnym wypadku nie zapewnia bezpośredniego dostępu do wartości samych obiektów . Jedyny dostęp do obiektów jest przez odniesienie do tego obiektu. Ponieważ do obiektów Java można zawsze uzyskać dostęp poprzez odwołanie, a nie bezpośrednio, często mówi się o polach i zmiennych oraz argumentach metod jako obiektach , a pedantycznie są to tylko odniesienia do obiektów .Zamieszanie wynika z tej (ściśle mówiąc niepoprawnej) zmiany w nomenklaturze.

Podczas wywoływania metody

  • W przypadku argumentów prymitywnych ( int, longitp.) Przekazywanie przez wartość jest rzeczywistą wartością operacji podstawowej (na przykład 3).
  • W przypadku obiektów przekazywanie przez wartość jest wartością odwołania do obiektu .

Więc jeśli masz doSomething(foo)i public void doSomething(Foo foo) { .. }dwie Foos skopiowały odniesienia, które wskazują na te same obiekty.

Oczywiście przekazywanie wartości przez odniesienie do obiektu wygląda bardzo podobnie (i jest w praktyce nie do odróżnienia) przekazywanie obiektu przez odniesienie.


7
Ponieważ wartości prymitywów są niezmienne (jak ciąg), różnica między tymi dwoma przypadkami nie jest tak naprawdę istotna.
Paŭlo Ebermann

4
Dokładnie. Dla wszystkiego, co można stwierdzić za pomocą obserwowalnego zachowania JVM, prymitywy mogą być przekazywane przez referencję i mogą żyć na stercie. Nie robią tego, ale nie da się tego w żaden sposób zaobserwować.
Grawitacja

6
prymitywy są niezmienne? czy to nowe w Javie 7?
user85421,

4
Wskaźniki są niezmienne, prymitywy ogólnie są zmienne. Łańcuch również nie jest prymitywny, to obiekt. Co więcej, podstawową strukturą ciągu jest zmienna tablica. Jedyną niezmienną rzeczą w tym jest długość, która jest nieodłączną naturą tablic.
kingfrito_5005,

3
To kolejna odpowiedź wskazująca na w dużej mierze semantyczny charakter argumentu. Definicja odniesienia podana w tej odpowiedzi sprawiłaby, że Java byłaby „przekazywana przez odniesienie”. Autor w istocie przyznaje tyle samo w ostatnim akapicie, stwierdzając, że jest „nie do odróżnienia w praktyce” od „przejścia przez odniesienie”. Wątpię, czy OP pyta z powodu chęci zrozumienia implementacji Java, ale raczej zrozumienia, jak poprawnie używać Java. Gdyby w praktyce było nie do odróżnienia, nie byłoby sensu się tym przejmować, a nawet myślenie o tym byłoby stratą czasu.
Loduwijk

331

Java przekazuje referencje według wartości.

Więc nie możesz zmienić referencji, która zostanie przekazana.


27
Ale rzecz, która się powtarza: „nie można zmienić wartości obiektów przekazywanych w argumentach”, jest wyraźnie nieprawdziwa. Możesz nie być w stanie sprawić, by odnosiły się do innego obiektu, ale możesz zmienić ich zawartość, wywołując ich metody. IMO oznacza to, że tracisz wszystkie zalety referencji i nie zyskujesz żadnych dodatkowych gwarancji.
Timmmm

34
Nigdy nie powiedziałem: „nie można zmienić wartości obiektów przekazywanych w argumentach”. Powiem „Nie można zmienić wartości odwołania do obiektu przekazanego jako argument metody”, co jest prawdziwym stwierdzeniem o języku Java. Oczywiście możesz zmienić stan obiektu (o ile nie jest niezmienny).
ScArcher2

20
Pamiętaj, że tak naprawdę nie można przekazywać obiektów w java; obiekty pozostają na stosie. Wskaźniki do obiektów mogą być przekazywane (które są kopiowane do ramki stosu dla wywoływanej metody). Dlatego nigdy nie zmieniasz przekazywanej wartości (wskaźnika), ale możesz ją śledzić i zmieniać rzecz na stercie, na którą wskazuje. To wartość przekazywana.
Scott Stanchfield

10
Wygląda na to, że właśnie przekazałeś referencję ? Mam zamiar poprzeć fakt, że Java jest nadal kopiowanym językiem pass-by-reference. Fakt, że jest to skopiowane odwołanie, nie zmienia terminologii. Oba REFERENCJE nadal wskazują na ten sam obiekt. To argument
purysty

3
Java nie przekazuje obiektu, przekazuje wartość wskaźnika do obiektu. Spowoduje to utworzenie nowego wskaźnika do położenia pamięci oryginalnego obiektu w nowej zmiennej. Jeśli zmienisz wartość (adres pamięci, na który wskazuje) tej zmiennej wskaźnika w metodzie, oryginalny wskaźnik użyty w wywołaniu metody pozostanie niezmodyfikowany. Jeśli nazywasz parametr referencją, to fakt, że jest on kopią referencji oryginalnej, a nie samej referencji, tak że istnieją teraz dwie referencje do obiektu, oznacza, że ​​jest on przekazywany przez wartość
theferrit32,

238

Mam ochotę kłócić się o „przekazanie przez odniesienie kontra przekazanie przez wartość” nie jest zbyt pomocne.

Jeśli powiesz: „Java to pass-by-cokolwiek (referencja / wartość)”, w obu przypadkach nie otrzymasz pełnej odpowiedzi. Oto kilka dodatkowych informacji, które, miejmy nadzieję, pomogą w zrozumieniu tego, co dzieje się w pamięci.

Crash oczywiście na stosie / stosie, zanim dojdziemy do implementacji Java: Wartości pojawiają się i znikają ze stosu w uporządkowany sposób, jak stos talerzy w stołówce. Pamięć w stercie (znana również jako pamięć dynamiczna) jest przypadkowa i niezorganizowana. JVM znajduje miejsce, gdzie tylko jest to możliwe, i zwalnia go, ponieważ zmienne, które go używają, nie są już potrzebne.

W porządku. Po pierwsze, lokalne prymitywy trafiają na stos. Więc ten kod:

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

skutkuje to:

prymitywy na stosie

Kiedy deklarujesz i tworzysz obiekt. Rzeczywisty obiekt trafia na stos. Co idzie na stos? Adres obiektu na stercie. Programiści C ++ nazywają to wskaźnikiem, ale niektórzy programiści Java są przeciwni słowu „wskaźnik”. Cokolwiek. Po prostu wiedz, że adres obiektu idzie na stos.

Tak jak:

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

ab * 7ch nie jest jeden!

Tablica jest obiektem, więc również trafia na stos. A co z obiektami w tablicy? Otrzymują własną przestrzeń sterty, a adres każdego obiektu trafia do tablicy.

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

Bracia Marks

Co zostaje przekazane po wywołaniu metody? Jeśli przekazujesz obiekt, tak naprawdę przekazujesz adres obiektu. Niektórzy mogą powiedzieć „wartość” adresu, a niektórzy twierdzą, że jest to tylko odniesienie do obiektu. Na tym polega geneza świętej wojny między zwolennikami „odniesienia” i „wartości”. To, co nazywasz, nie jest tak ważne, jak to, że rozumiesz, że przekazywany jest adres do obiektu.

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

Tworzony jest jeden ciąg, a miejsce na niego jest przydzielane na stercie, a adres ciągu jest przechowywany na stosie i podany identyfikator hisName, ponieważ adres drugiego ciągu jest taki sam jak pierwszy, nie jest tworzony nowy ciąg i nie przydzielono nowego miejsca na stercie, ale na stosie utworzono nowy identyfikator. Następnie wywołujemy shout(): tworzona jest nowa ramka stosu, tworzony jest nowy identyfikator namei przypisywany adres już istniejącego ciągu.

la da di da da da da

A więc wartość, odniesienie? Mówisz „ziemniak”.


7
Powinieneś jednak pójść za bardziej złożonym przykładem, w którym funkcja wydaje się zmieniać zmienną, do której adresu ma odwołanie.
Brian Peterson

34
Ludzie nie tańczą wokół prawdziwego problemu stosu kontra sterty, ponieważ to nie jest prawdziwy problem. Jest to co najmniej szczegół implementacji, aw najgorszym wręcz błędny. (Jest całkiem możliwe, że obiekty znajdują się na stosie; Google „analiza ucieczki”. A ogromna liczba obiektów zawiera prymitywy, które prawdopodobnie nie znajdują się na stosie.) Prawdziwym problemem jest dokładnie różnica między typami referencji a typami wartości - w szczególności, że wartość zmiennej typu odwołania jest odwołaniem, a nie przedmiotem, do którego się odnosi.
cHao,

8
Jest to „szczegół implementacji”, ponieważ Java nigdy nie jest wymagana, aby pokazać, gdzie obiekt mieszka w pamięci, i wydaje się zdeterminowana, aby uniknąć wycieku tych informacji. Może umieścić obiekt na stosie, a ty nigdy się nie dowiesz. Jeśli cię to obchodzi, skupiasz się na złej rzeczy - w tym przypadku oznacza to ignorowanie prawdziwego problemu.
cHao,

10
W obu przypadkach „prymitywy idą na stos” jest niepoprawne. Pierwotne zmienne lokalne trafiają na stos. (Jeśli nie zostały zoptymalizowane, oczywiście.) Ale tak samo, jak lokalne zmienne odniesienia . A prymitywne elementy zdefiniowane w obiekcie żyją wszędzie tam, gdzie żyje obiekt.
cHao,

9
Zgadzam się z komentarzami tutaj. Stos / kupka jest kwestią poboczną i nie dotyczy. Niektóre zmienne mogą znajdować się na stosie, niektóre są w pamięci statycznej (zmienne statyczne), a wiele na żywo na stercie (wszystkie zmienne składowe obiektu). ŻADNA z tych zmiennych nie może być przekazana przez referencję: z wywoływanej metody NIGDY nie można zmienić wartości zmiennej przekazywanej jako argument. Dlatego w Javie nie ma przekazywania przez odniesienie.
fishinear

195

Aby pokazać kontrast, porównaj następujące fragmenty kodu C ++ i Java :

W C ++: Uwaga: Zły kod - wycieki pamięci! Ale to pokazuje sens.

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

W Javie

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 ma tylko dwa typy przekazywania: według wartości dla typów wbudowanych i według wartości wskaźnika dla typów obiektów.


7
+1 Dodałbym również Dog **objPtrPtrdo przykładu C ++, w ten sposób możemy zmodyfikować to, na co wskazuje wskaźnik.
Amro

1
Ale to nie odpowiada na dane pytanie w Javie. W rzeczywistości wszystko w metodzie Java to Pass By Value, nic więcej.
tauitdnmd

2
Ten przykład tylko dowodzi, że Java używa bardzo odpowiednika wskaźnika w C nie? Gdzie przekazywanie wartości w C jest w zasadzie tylko do odczytu? Na koniec cały ten wątek jest niezrozumiały
amdev


166

Zasadniczo zmiana przypisania parametrów obiektu nie wpływa na argument, np.

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

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

wydrukuje "Hah!"zamiast null. Powodem tego jest to, że barjest to kopia wartości baz, która jest tylko odniesieniem "Hah!". Gdyby to było rzeczywiste odniesienie, foozdefiniowałoby to bazna nowo null.


8
Wolałbym powiedzieć, że bar jest kopią bazowej referencji (lub bazowego aliasu), która początkowo wskazuje na ten sam obiekt.
MaxZoom

czy nie ma żadnej różnicy między klasą String a wszystkimi innymi klasami?
Mehdi Karamosly,

152

Nie mogę uwierzyć, że nikt jeszcze nie wspominał Barbary Liskov. Kiedy zaprojektowała CLU w 1974 r., Napotkała ten sam problem terminologiczny i wymyśliła pojęcie połączenia przez udostępnianie (znane również jako połączenie przez udostępnianie obiektów i połączenie według obiektu ) dla tego konkretnego przypadku „wywołanie według wartości, gdzie wartość jest referencja".


3
Podoba mi się to rozróżnienie w nomenklaturze. To niefortunne, że Java obsługuje wywołanie przez współdzielenie obiektów, ale nie wywołuje według wartości (podobnie jak C ++). Java obsługuje wywołanie według wartości tylko dla pierwotnych typów danych, a nie dla złożonych typów danych.
Derek Mahar

2
Naprawdę nie sądzę, że potrzebowaliśmy dodatkowego terminu - jest to po prostu wartość dodana dla określonego rodzaju wartości. Czy dodanie „wywołania przez prymitywne” dodałoby jakieś wyjaśnienie?
Scott Stanchfield


Czy mogę przekazywać odniesienia przez udostępnianie jakiegoś globalnego obiektu kontekstu, a nawet przekazywanie obiektu kontekstu zawierającego inne odniesienia? Nadal przekazuję wartość, ale przynajmniej mam dostęp do referencji, które mogę modyfikować i wskazywać na coś innego.
YoYo

1
@Sanjeev: udostępnianie call-by-object jest szczególnym przypadkiem przekazywania wartości. Jednak wiele osób twierdzi gwałtownie, że Java (i podobne języki, takie jak Python, Ruby, ECMAScript, Smalltalk) są przekazywane przez odniesienia. Wolałbym też nazywać to „wartość po wartości”, ale dzielenie się wywołaniem obiektu wydaje się rozsądnym terminem, na który nawet ludzie, którzy twierdzą, że nie jest to wartość zgodna, mogą się zgodzić.
Jörg W Mittag

119

Sedno sprawy polega na tym, że słowo referencja w wyrażeniu „pass by referencja” oznacza coś zupełnie innego niż zwykłe znaczenie słowa referencyjnego w Javie.

Zwykle w Javie odwołanie oznacza odniesienie do obiektu . Ale terminy techniczne przekazywane przez odniesienie / wartość z teorii języka programowania mówią o odwołaniu do komórki pamięci przechowującej zmienną , co jest czymś zupełnie innym.


7
@Gevorg - Co to jest „NullPointerException”?
Hot Licks

5
@Hot: Niestety nazwany wyjątek sprzed czasu, gdy Java zdecydowała się na jasną terminologię. Semantycznie równoważny wyjątek w języku c # nazywa się NullReferenceException.
JacquesB

4
Zawsze wydawało mi się, że użycie „odniesienia” w terminologii Java jest wpływem utrudniającym zrozumienie.
Hot Licks,

Nauczyłem się nazywać te tak zwane „wskaźniki do obiektów” jako „uchwyt do obiektu”. Ta zmniejszona dwuznaczność.
moronkreacionz

86

W Javie wszystko jest odniesieniem, więc kiedy masz coś takiego: Point pnt1 = new Point(0,0);Java robi następujące:

  1. Tworzy nowy obiekt Point
  2. Tworzy nowe odniesienie do punktu i inicjuje to odniesienie do punktu (patrz) na wcześniej utworzonym obiekcie Point.
  3. Odtąd poprzez życie obiektu Point uzyskasz dostęp do tego obiektu poprzez odniesienie pnt1. Możemy więc powiedzieć, że w Javie manipulujesz obiektem poprzez jego odwołanie.

wprowadź opis zdjęcia tutaj

Java nie przekazuje argumentów metod przez odwołanie; przekazuje je według wartości. Wykorzystam przykład z tej strony :

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

Przebieg programu:

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

Tworzenie dwóch różnych obiektów Point z powiązanymi dwoma różnymi odniesieniami. wprowadź opis zdjęcia tutaj

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

Zgodnie z oczekiwaniami, wynik będzie:

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

W tym wierszu „pass-by-value” wchodzi do gry ...

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

Referencje pnt1i pnt2przekazywane przez wartość metody skomplikowane, co oznacza, że teraz wasze referencje pnt1i pnt2mają swoje copiesnazwanych arg1i arg2.so pnt1i arg1 punkty do tego samego obiektu. (To samo dla pnt2i arg2) wprowadź opis zdjęcia tutaj

W trickymetodzie:

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

wprowadź opis zdjęcia tutaj

Dalej w tricky metodzie

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

Tutaj najpierw tworzysz nowy temppunkt odniesienia, który będzie wskazywał w tym samym miejscu jak arg1punkt odniesienia. Następnie przenieś odniesienie, arg1aby wskazywało to samo miejsce, co arg2odniesienie. Wreszcie arg2będzie wskazywać na tym samym miejscu jak temp.

wprowadź opis zdjęcia tutaj

Stąd zakres trickymetody nie ma, a ty nie masz dostępu do więcej odniesień: arg1, arg2, temp. Ważna uwaga jest jednak taka, że ​​wszystko, co robisz z tymi odniesieniami, gdy są „w życiu”, na stałe wpłynie na obiekt, na który są skierowane .

Tak więc po wykonaniu metody tricky, gdy wrócisz main, masz następującą sytuację: wprowadź opis zdjęcia tutaj

Zatem teraz całkowite wykonanie programu będzie:

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

7
W java, kiedy mówisz „W java wszystko jest odniesieniem” masz na myśli wszystko obiekty są przekazywane przez referencję. Podstawowe typy danych nie są przekazywane przez odwołanie.
Eric

Jestem w stanie wydrukować zamienioną wartość główną metodą. w metodzie trickey dodaj następującą instrukcję arg1.x = 1; arg1.y = 1; arg2.x = 2; arg2.y = 2;, ponieważ arg1 teraz trzyma referencję pnt2, a arg2 trzyma teraz referencję pnt1, więc jego drukowanieX1: 2 Y1: 2 X2: 1 Y2: 1
Shahid Ghafoor

85

Java jest zawsze przekazywana przez wartość, a nie przez referencję

Przede wszystkim musimy zrozumieć, jakie są wartości przekazane i referencyjne.

Przekaż przez wartość oznacza, że ​​wykonujesz kopię w pamięci wartości parametru, który jest przekazywany. Jest to kopia zawartości rzeczywistego parametru .

Przekazywanie przez odniesienie (zwane również przekazywaniem przez adres) oznacza, że ​​przechowywana jest kopia adresu rzeczywistego parametru .

Czasami Java może dać złudzenie przejścia przez odniesienie. Zobaczmy, jak to działa, korzystając z poniższego przykładu:

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

Dane wyjściowe tego programu to:

changevalue

Rozumiemy krok po kroku:

Test t = new Test();

Jak wszyscy wiemy, utworzy obiekt na stercie i zwróci wartość odniesienia z powrotem do t. Załóżmy na przykład, że wartość t jest 0x100234(nie znamy rzeczywistej wartości wewnętrznej JVM, to tylko przykład).

pierwsza ilustracja

new PassByValue().changeValue(t);

Przekazując odwołanie t do funkcji, nie przekaże bezpośrednio rzeczywistej wartości odniesienia testu obiektu, ale utworzy kopię t, a następnie przekaże ją do funkcji. Ponieważ przekazuje wartość , przekazuje kopię zmiennej zamiast faktycznego odwołania do niej. Ponieważ powiedzieliśmy, że wartość t była 0x100234, zarówno t i f będą miały tę samą wartość, a zatem będą wskazywać na ten sam obiekt.

druga ilustracja

Jeśli zmienisz cokolwiek w funkcji za pomocą odwołania f, zmodyfikuje to istniejącą zawartość obiektu. Dlatego otrzymaliśmy wyjście changevalue, które jest aktualizowane w funkcji.

Aby to lepiej zrozumieć, rozważ następujący przykład:

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

Czy to rzuci NullPointerException? Nie, ponieważ przekazuje tylko kopię odwołania. W przypadku przekazania przez referencję mógł on rzucićNullPointerException , jak pokazano poniżej:

trzecia ilustracja

Mam nadzieję, że to pomoże.


71

Odwołanie jest zawsze wartością, gdy jest reprezentowane, bez względu na używany język.

Wychodząc poza pole widzenia, spójrzmy na Zgromadzenie lub zarządzanie pamięcią niskiego poziomu. Na poziomie procesora odniesienie do czegokolwiek natychmiast staje się wartością, jeśli zostanie zapisane w pamięci lub w jednym z rejestrów procesora. (Dlatego wskaźnik jest dobrą definicją. Jest to wartość, która ma jednocześnie cel).

Dane w pamięci mają lokalizację i w tej lokalizacji znajduje się wartość (bajt, słowo, cokolwiek). W asemblerze mamy wygodne rozwiązanie do nadania nazwy określonej lokalizacji (znanej również jako zmienna), ale podczas kompilacji kodu asembler po prostu zastępuje nazwę na wskazaną lokalizację, tak jak przeglądarka zastępuje nazwy domen adresami IP.

Zasadniczo technicznie niemożliwe jest przekazanie odniesienia do czegokolwiek w dowolnym języku bez reprezentowania go (gdy staje się ono natychmiast wartością).

Powiedzmy, że mamy zmienną Foo, jej lokalizacja znajduje się na 47-tym bajcie w pamięci, a jej wartość wynosi 5. Mamy inną zmienną Ref2Foo, która ma 223-ty bajt w pamięci, a jej wartość to 47. Ten Ref2Foo może być zmienną techniczną , nie zostały wyraźnie utworzone przez program. Jeśli popatrzysz na 5 i 47 bez żadnych innych informacji, zobaczysz tylko dwie Wartości . Jeśli użyjesz ich jako referencji, aby dotrzeć do 5nas, musimy podróżować:

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

Tak działają tabele skoków.

Jeśli chcemy wywołać metodę / funkcję / procedurę z wartością Foo, istnieje kilka możliwych sposobów przekazania zmiennej do metody, w zależności od języka i kilku trybów wywoływania metody:

  1. 5 zostaje skopiowany do jednego z rejestrów CPU (tj. EAX).
  2. 5 dostaje PUSHd do stosu.
  3. 47 zostaje skopiowany do jednego z rejestrów CPU
  4. 47 PUSHd do stosu.
  5. 223 zostaje skopiowany do jednego z rejestrów CPU.
  6. 223 dostaje PUSHd do stosu.

W każdym przypadku powyżej wartości - kopii istniejącej wartości - została utworzona, teraz jest to metoda odbierająca, aby ją obsłużyć. Kiedy piszesz „Foo” w metodzie, jest ono albo odczytywane z EAX, albo automatycznie usuwane lub podwójnie usuwane, proces zależy od tego, jak działa język i / lub co dyktuje typ Foo. Jest to ukryte przed deweloperem, dopóki nie obejdzie procesu dereferencji. Więc odniesienia jest wartość , gdy reprezentował, ponieważ odniesienia jest to wartość, która ma zostać przetworzone (na poziomie językowym).

Teraz przekazaliśmy Foo do metody:

  • w przypadku 1. i 2. zmiana Foo ( Foo = 9) wpływa tylko na zasięg lokalny, ponieważ masz kopię Wartości. Wewnątrz metody nie możemy nawet ustalić, gdzie w pamięci znajdował się oryginalny Foo.
  • w przypadku 3. i 4. jeśli użyjesz domyślnych konstrukcji języka i zmienisz Foo ( Foo = 11), może to zmienić Foo globalnie (zależy od języka, tj. Java lub jak procedure findMin(x, y, z: integer;var m Pascala : integer);). Jeśli jednak język pozwala na obejście procesu wyłuskiwania, możesz to zmienić 47, powiedzmy 49. W tym momencie Foo wydaje się być zmienione, jeśli go przeczytasz, ponieważ zmieniłeś na niego lokalny wskaźnik . A jeśli zmodyfikujesz tego Foo w metodzie ( Foo = 12), prawdopodobnie FUBAR wykonasz program (inaczej segfault), ponieważ napiszesz w innej pamięci niż oczekiwano, możesz nawet zmodyfikować obszar przeznaczony do przechowywania plików wykonywalnych program i pisanie do niego zmodyfikuje działający kod (Foo nie jest teraz w 47). ALE wartość Foo wynosząca47nie zmienił się globalnie, tylko ten wewnątrz metody, ponieważ 47był także kopią metody.
  • w przypadku 5. i 6. jeśli zmodyfikujesz 223wewnątrz metody, spowoduje to taki sam chaos jak w 3. lub 4. (wskaźnik wskazujący teraz na złą wartość, który ponownie jest używany jako wskaźnik), ale nadal jest to lokalny problem, ponieważ 223 zostało skopiowane . Jednakże, jeśli jesteś w stanie wyłuskać Ref2Foo(to znaczy 223), dotrzeć i zmodyfikować wskazaną wartość 47, powiedzmy, to 49, wpłynie to na Foo globalnie , ponieważ w tym przypadku metody otrzymały kopię, 223 ale odwołanie 47istnieje tylko raz, i zmiana tego aby 49doprowadzi każdą Ref2Foopodwójnego wyłuskania do niewłaściwej wartości.

Poszukując nieistotnych szczegółów, nawet języki, które przechodzą przez odniesienie, przekażą wartości do funkcji, ale te funkcje wiedzą, że muszą go użyć do celów dereferencji. Ta wartość pass-the-referen-as-value jest po prostu ukryta przed programistą, ponieważ jest praktycznie bezużyteczna, a terminologia jest tylko pass-by-referencją .

Ścisłe przekazywanie przez wartość jest również bezużyteczne, oznaczałoby to, że tablica 100 MB powinna być kopiowana za każdym razem, gdy wywołujemy metodę z tablicą jako argumentem, dlatego Java nie może być ściśle przekazywana przez wartość. Każdy język przekazałby odwołanie do tej ogromnej tablicy (jako wartość) i albo stosuje mechanizm kopiowania przy zapisie, jeśli tablica może być zmieniona lokalnie wewnątrz metody, albo pozwala metodzie (podobnie jak Java) na globalną modyfikację tablicy (z widok dzwoniącego) i kilka języków pozwala modyfikować wartość samego odwołania.

W skrócie i we własnej terminologii Java, Java jest wartością przekazywaną, gdzie wartość może być: albo wartością rzeczywistą, albo wartością, która jest reprezentacją odwołania .


1
W języku z przekazywaniem odniesienia przekazywana rzecz (odniesienie) jest efemeryczna; odbiorca nie powinien go „skopiować”. W Javie przekazywanie tablicy jest przekazywane jako „identyfikator obiektu” - odpowiednik kartki papieru z napisem „Obiekt # 24601”, kiedy 24601. skonstruowany obiekt był tablicą. Odbiorca może skopiować „Obiekt # 24601” w dowolnym miejscu, a każdy, kto ma kartkę z napisem „Obiekt # 24601”, może zrobić wszystko, co chce, z dowolnym elementem tablicy. Przekazany fragment bitów nie powiedziałby oczywiście „Obiekt # 24601”, ale ...
supercat

... kluczową kwestią jest to, że odbiorca identyfikatora obiektu identyfikującego tablicę może przechowywać ten identyfikator obiektu, gdziekolwiek chce i dać go komukolwiek chce, a każdy odbiorca będzie mógł uzyskać dostęp do tablicy lub ją modyfikować, ilekroć będzie chce. Natomiast jeśli tablica byłaby przekazywana przez referencję w języku takim jak Pascal, który obsługuje takie rzeczy, wywoływana metoda mogłaby zrobić z tablicą wszystko, co chciała, ale nie mogłaby przechowywać referencji w sposób pozwalający na modyfikację tablicy przez kod po powrocie.
supercat

Rzeczywiście, w Pascalu ty może uzyskać adres każdej zmiennej, czy to przekazywane przez referencję lub lokalnie kopiowane, addrrobi to bez typu, @robi to z typu i można zmodyfikować zmienną odwołuje później (z wyjątkiem lokalnie skopiowane z nich). Ale nie rozumiem, dlaczego to zrobiłbyś. Ten odcinek papieru w twoim przykładzie (Obiekt # 24601) jest odniesieniem, jego celem jest pomoc w znalezieniu tablicy w pamięci, nie zawiera ona żadnych danych tablicy w sobie. Jeśli zrestartujesz program, ta sama tablica może otrzymać inny identyfikator obiektu, nawet jeśli jego zawartość będzie taka sama jak w poprzednim uruchomieniu.
karatedog

Myślałem, że operator „@” nie jest częścią standardowego Pascala, ale został zaimplementowany jako wspólne rozszerzenie. Czy to część standardu? Chodziło mi o to, że w języku z prawdziwym przekazywaniem i bez możliwości konstruowania nie efemerycznego wskaźnika do efemerycznego obiektu, kodu, który zawiera jedyne odniesienie do tablicy, w dowolnym miejscu we wszechświecie, przed pominięciem tablicy przez referencja może wiedzieć, że o ile odbiorca nie „oszuka”, nadal będzie przechowywać jedyne referencje później. Jedynym bezpiecznym sposobem na osiągnięcie tego w Javie byłoby zbudowanie obiektu tymczasowego ...
supercat

... który zawiera AtomicReferencei nie ujawnia odwołania ani jego celu, ale zamiast tego zawiera metody robienia rzeczy z celem; po zwróceniu kodu, do którego obiekt został przekazany, AtomicReference[do którego jego twórca prowadził bezpośrednie odniesienie] należy unieważnić i porzucić. Zapewniłoby to właściwą semantykę, ale byłoby powolne i niepewne.
supercat

70

Java to wywołanie według wartości

Jak to działa

  • Zawsze przekazujesz kopię bitów wartości referencji!

  • Jeśli jest to prymitywny typ danych, te bity zawierają wartość samego prymitywnego typu danych, dlatego jeśli zmienimy wartość nagłówka wewnątrz metody, wówczas nie będzie odzwierciedlać zmian na zewnątrz.

  • Jeśli jest to typ danych obiektu, taki jak Foo foo = new Foo (), to w tym przypadku kopia adresu obiektu przechodzi jak skrót do pliku, załóżmy, że mamy plik tekstowy abc.txt na C: \ desktop i załóżmy, że utworzymy skrót do ten sam plik i umieść go w skrócie C: \ desktop \ abc-skrót, aby po uzyskaniu dostępu do pliku z C: \ desktop \ abc.txt napisać „Przepełnienie stosu” i zamknąć plik, a następnie ponownie otworzyć plik ze skrótu write to największa społeczność internetowa, z której programiści mogą się uczyć. to całkowita zmiana pliku, „Stack Overflow to największa społeczność internetowa dla programistów”co oznacza, że ​​nie ma znaczenia, gdzie otworzysz plik, za każdym razem, gdy uzyskujemy dostęp do tego samego pliku, tutaj możemy założyćFoo jako plik i załóżmy, że foo jest przechowywany w 123hd7h (oryginalny adres, taki jak C: \ desktop \ abc.txt ) i 234jdid (skopiowany adres, taki jak C: \ desktop \ abc-shortcut, który faktycznie zawiera oryginalny adres pliku). Aby uzyskać lepsze zrozumienie, zrób plik skrótu i ​​poczuj ...


66

Istnieją już świetne odpowiedzi na to pytanie. Chciałem wnieść niewielki wkład, dzieląc się bardzo prostym przykładem (który skompiluje) przeciwstawiającym się zachowaniom między Pass-by-reference w c ++ i Pass-by-value w Javie.

Kilka punktów:

  1. Termin „odniesienie” jest przeciążony dwoma odrębnymi znaczeniami. W Javie oznacza to po prostu wskaźnik, ale w kontekście „Pass-by-reference” oznacza uchwyt pierwotnej zmiennej, która została przekazana.
  2. Java to Pass-by-value . Java jest potomkiem C (między innymi językami). Przed C kilka wcześniejszych (ale nie wszystkie) języków, takich jak FORTRAN i COBOL, obsługiwało PBR, ale C nie. PBR umożliwił tym innym językom wprowadzanie zmian do przekazywanych zmiennych wewnątrz podprogramów. Aby osiągnąć to samo (tj. Zmienić wartości zmiennych wewnątrz funkcji), programiści C przekazali wskaźniki do zmiennych w funkcje. Języki zainspirowane C, takie jak Java, zapożyczyły ten pomysł i nadal przekazują wskaźnik do metod tak jak C, z tym wyjątkiem, że Java wywołuje swoje wskaźniki Referencje. Ponownie jest to inne użycie słowa „Odniesienie” niż w „Pass-By-Reference”.
  3. C ++ zezwala na przekazanie przez referencję poprzez zadeklarowanie parametru referencyjnego za pomocą znaku „&” (który jest tym samym znakiem używanym do wskazania „adresu zmiennej” zarówno w C, jak i C ++). Na przykład, jeśli przekażemy wskaźnik przez referencję, parametr i argument nie wskazują tylko tego samego obiektu. Są raczej tą samą zmienną. Jeśli jeden zostanie ustawiony na inny adres lub na zero, to samo zrobi drugi.
  4. W poniższym przykładzie C ++ przekazuję wskaźnik do łańcucha zakończonego znakiem null przez referencję . W poniższym przykładzie Javy przekazuję wartość referencyjną Java do łańcucha znaków (ponownie tak samo jak wskaźnik do łańcucha znaków) według wartości. Zwróć uwagę na wynik w komentarzach.

C ++ przekazuje przykład referencji:

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 przekazuje „referencję Java” według przykładu wartości

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

EDYTOWAĆ

Kilka osób napisało komentarze, które wydają się wskazywać, że albo nie patrzą na moje przykłady, albo nie dostają przykładu c ++. Nie jestem pewien, gdzie jest rozłączenie, ale zgadywanie przykładu c ++ nie jest jasne. Podaję ten sam przykład w pascal, ponieważ myślę, że przekazanie przez odniesienie wygląda na pascal bardziej przejrzyste, ale mogę się mylić. Być może bardziej mylę ludzi; Mam nadzieję, że nie.

W pascal parametry przekazywane przez odniesienie nazywane są „parametrami var”. W poniższej procedurze setToNil zwróć uwagę na słowo kluczowe „var”, które poprzedza parametr „ptr”. Gdy wskaźnik zostanie przekazany do tej procedury, zostanie przekazany przez odniesienie . Zwróć uwagę na zachowanie: gdy ta procedura ustawia ptr na zero (to znaczy, że Pascal mówi dla NULL), ustawi argument na zero - nie możesz tego zrobić w Javie.

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.

EDYCJA 2

Niektóre fragmenty „Języka programowania Java” Kena Arnolda, Jamesa Goslinga (faceta, który wynalazł Javę) i Davida Holmesa, rozdział 2, sekcja 2.6.5

Wszystkie parametry metod są przekazywane „według wartości” . Innymi słowy, wartości zmiennych parametrów w metodzie są kopiami wywoływacza określonego jako argumenty.

Dalej mówi o przedmiotach. . .

Należy zauważyć, że gdy parametr jest odwołaniem do obiektu, to odwołanie do obiektu, a nie sam obiekt, jest przekazywane „przez wartość” .

Pod koniec tej samej sekcji wygłasza szersze stwierdzenie, że java jest przekazywana tylko przez wartość, a nigdy przez referencję.

Język programowania Java nie przekazuje obiektów przez odniesienie; to przechodzi przez wartość odniesienia obiektu . Ponieważ dwie kopie tego samego odwołania odnoszą się do tego samego rzeczywistego obiektu, zmiany dokonane za pomocą jednej zmiennej odniesienia są widoczne przez drugą. Jest dokładnie jeden tryb przekazywania parametrów - przekazywanie według wartości - i który pomaga zachować prostotę.

Ta sekcja książki zawiera doskonałe wyjaśnienie przekazywania parametrów w Javie oraz rozróżnienia między przekazywaniem referencji a przekazywaniem wartości przez autora Javy. Zachęcam wszystkich do przeczytania go, zwłaszcza jeśli nadal nie jesteś przekonany.

Myślę, że różnica między tymi dwoma modelami jest bardzo subtelna i chyba że wykonałeś programowanie w miejscu, w którym faktycznie korzystałeś z metody referencji, łatwo przeoczyć różnice między dwoma modelami.

Mam nadzieję, że to rozstrzygnie debatę, ale prawdopodobnie nie.

EDYCJA 3

Może mam trochę obsesji na punkcie tego postu. Prawdopodobnie dlatego, że uważam, że twórcy Javy nieumyślnie rozpowszechniają dezinformację. Gdyby zamiast używać słowa „odniesienie” do wskaźników użyliby czegoś innego, powiedzmy dingleberry, nie byłoby problemu. Można powiedzieć: „Java mija dingleberry według wartości, a nie przez odniesienie”, i nikt by się nie pomylił.

To jest powód, dla którego tylko programiści Java mają z tym problem. Patrzą na słowo „odniesienie” i myślą, że wiedzą dokładnie, co to znaczy, więc nawet nie zadają sobie trudu, by rozważyć przeciwny argument.

W każdym razie zauważyłem komentarz w starszym poście, który stworzył analogię z balonem, która naprawdę mi się podobała. Do tego stopnia, że ​​postanowiłem skleić trochę clipartów, aby stworzyć zestaw kreskówek ilustrujących tę kwestię.

Przekazywanie referencji przez wartość - zmiany w referencji nie są odzwierciedlane w zakresie obiektu wywołującego, ale są zmiany w obiekcie. Jest tak, ponieważ odwołanie jest kopiowane, ale zarówno oryginał, jak i kopia odnoszą się do tego samego obiektu. Przekazywanie odwołań do obiektów według wartości

Przekaż przez referencję - Nie ma kopii referencji. Pojedyncze odwołanie jest wspólne zarówno dla osoby wywołującej, jak i wywoływanej funkcji. Wszelkie zmiany odwołania lub danych obiektu są odzwierciedlane w zakresie dzwoniącego. Przekaż przez odniesienie

EDYCJA 4

Widziałem posty na ten temat, które opisują niskopoziomową implementację przekazywania parametrów w Javie, co moim zdaniem jest świetne i bardzo pomocne, ponieważ tworzy konkretny abstrakcyjny pomysł. Jednak dla mnie pytanie dotyczy bardziej zachowania opisanego w specyfikacji języka niż technicznej implementacji tego zachowania. To jest ćwiczenie ze specyfikacji języka Java, sekcja 8.4.1 :

Gdy wywoływana jest metoda lub konstruktor (§15.12), wartości rzeczywistych wyrażeń argumentowych inicjują nowo utworzone zmienne parametrów, każdego z zadeklarowanego typu, przed wykonaniem treści metody lub konstruktora. Identyfikator występujący w DeclaratorId może być użyty jako prosta nazwa w treści metody lub konstruktora w celu odniesienia do parametru formalnego.

Co oznacza, że ​​Java wykonuje kopię przekazanych parametrów przed wykonaniem metody. Podobnie jak większość ludzi, którzy studiowali kompilatorów w college'u, użyłem „smok Book” , która jest książka kompilatory. Ma dobry opis „Call-by-value” i „Call-by-Reference” w rozdziale 1. Opis Call-by-Value dokładnie pasuje do specyfikacji Java.

Kiedy studiowałem kompilatory w latach 90-tych, korzystałem z pierwszego wydania książki z 1986 roku, która wyprzedziła Javę o około 9 lub 10 lat. Jednak właśnie natknąłem się na kopię 2. Edycji z 2007 roku, która faktycznie wspomina o Javie! Rozdział 1.6.6, oznaczony jako „Mechanizmy przekazywania parametrów”, całkiem ładnie opisuje przekazywanie parametrów. Oto fragment pod nagłówkiem „Call-by-value”, który wspomina o Javie:

W call-by-value rzeczywisty parametr jest oceniany (jeśli jest wyrażeniem) lub kopiowany (jeśli jest zmienną). Wartość jest umieszczana w miejscu należącym do odpowiedniego parametru formalnego wywoływanej procedury. Ta metoda jest używana w C i Java i jest powszechną opcją w C ++, a także w większości innych języków.


4
Szczerze mówiąc, możesz uprościć tę odpowiedź, mówiąc, że Java przekazuje wartość tylko dla typów pierwotnych. Wszystko, co dziedziczy po Object, jest skutecznie przekazywane przez referencję, gdzie referencją jest przekazywany wskaźnik.
Scuba Steve

1
@JuanMendes, właśnie użyłem C ++ jako przykładu; Mógłbym podać przykłady w innych językach. Termin „Przekaż przez odniesienie” istniał na długo przed istnieniem C ++. Jest to termin z podręcznika o bardzo szczegółowej definicji. Z definicji Java nie jest przekazywana przez odniesienie. Możesz nadal używać tego terminu, jeśli chcesz, ale twoje użycie nie będzie zgodne z definicją podręcznika. To nie tylko wskaźnik do wskaźnika. Jest to konstrukcja dostarczona przez język, aby umożliwić „Pass By Reference”. Przyjrzyj się uważnie mojemu przykładowi, ale nie wierz mi na słowo i sam go sprawdź.
Sanjeev

2
@AutomatedMike Myślę, że opisywanie Javy jako „pass-by-reference” jest również mylące, ich odniesienia są niczym więcej jak tylko wskazówkami. Jak Sanjeev napisał wartość, referencje i wskaźniki są terminami podręczników, które mają swoje własne znaczenie bez względu na to, czego używają twórcy Java.
Jędrzej Dudkiewicz

1
@ArtanisZeratul punkt jest subtelny, ale nie skomplikowany. Proszę spojrzeć na kreskówkę, którą opublikowałem. Czułem, że jest to dość proste do naśladowania. To, że Java jest wartością przekazywaną, nie jest tylko opinią. Jest to zgodne z definicją przekazu wartości w podręczniku. Ponadto, „ludzie, którzy zawsze twierdzą, że to wartość”, to informatycy tacy jak James Gosling, twórca Javy. Proszę zobaczyć cytaty z jego książki w sekcji „Edytuj 2” w moim poście.
Sanjeev

1
Cóż za świetna odpowiedź, „przekazywanie przez odniesienie” nie istnieje w java (i innych językach, takich jak JS).
newfolder

56

O ile mi wiadomo, Java zna tylko połączenia według wartości. Oznacza to, że dla prymitywnych typów danych będziesz pracować z kopią, a dla obiektów będziesz pracować z kopią odwołania do obiektów. Myślę jednak, że są pewne pułapki; na przykład to nie zadziała:

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

Spowoduje to zapełnienie Hello World, a nie World Hello, ponieważ w funkcji wymiany używasz kopii, które nie mają wpływu na odwołania w głównym. Ale jeśli twoich obiektów nie można zmienić na stałe, możesz to zmienić na przykład:

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

Spowoduje to wypełnienie Hello World w wierszu poleceń. Jeśli zmienisz StringBuffer w String, wygeneruje on tylko Hello, ponieważ String jest niezmienny. Na przykład:

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

Jednak możesz utworzyć opakowanie dla takiego ciągu, które pozwoliłoby go używać z ciągami:

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

edycja: wierzę, że jest to również powód, aby używać StringBuffer, jeśli chodzi o „dodawanie” dwóch Ciągów, ponieważ można modyfikować oryginalny obiekt, czego nie można zrobić w przypadku niezmiennych obiektów, takich jak Ciąg.


+1 za test wymiany - prawdopodobnie najłatwiejszy i najłatwiejszy sposób na rozróżnienie między przekazywaniem przez referencję a przekazywaniem odniesienia wartością . Jeśli możesz łatwo napisać funkcję, swap(a, b)która (1) zamienia ai bz POV dzwoniącego, (2) jest niezależna od typu w takim zakresie, w jakim pozwala na to pisanie statyczne (co oznacza, że ​​używanie go z innym typem nie wymaga niczego więcej niż zmiana deklarowanych typów ai b) , i (3) nie wymaga od osoby wywołującej jawnego przekazania wskaźnika lub nazwy, wówczas język obsługuje przekazywanie przez referencję.
cHao

„... dla prymitywnych typów danych będziesz pracować z kopią, a dla obiektów będziesz pracować z kopią odwołania do obiektów” - doskonale napisane!
Raúl

55

Nie, nie przekazuje się go przez odniesienie.

Java jest przekazywana według wartości zgodnie ze specyfikacją języka Java:

Gdy wywoływana jest metoda lub konstruktor (§15.12), wartości rzeczywistych wyrażeń argumentowych inicjują nowo utworzone zmienne parametrów , każdego z zadeklarowanego typu, przed wykonaniem treści metody lub konstruktora. Identyfikator występujący w DeclaratorId może być użyty jako prosta nazwa w treści metody lub konstruktora w celu odniesienia do parametru formalnego .


52

Pozwól mi spróbować wyjaśnić moje zrozumienie na podstawie czterech przykładów. Java jest przekazywana według wartości, a nie przekazywana przez referencję

/ **

Przekaż według wartości

W Javie wszystkie parametry są przekazywane przez wartość, tzn. Przypisanie argumentu metody nie jest widoczne dla dzwoniącego.

* /

Przykład 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";
    }
}

Wynik

output : output
value : Nikhil
valueflag : false

Przykład 2:

/ ** * * Przekaż według wartości * * /

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

Wynik

output : output
value : Nikhil
valueflag : false

Przykład 3:

/ ** Ten „Pass By Value” przypomina „Pass By Reference”

Niektórzy twierdzą, że typy pierwotne i „Łańcuch” są „przekazywane przez wartość”, a obiekty są „przekazywane przez odniesienie”.

Ale z tego przykładu możemy zrozumieć, że jest to w rzeczywistości tylko wartość, pamiętając, że tutaj przekazujemy referencję jako wartość. tzn .: odniesienie jest przekazywane przez wartość. Dlatego mogą się zmieniać i nadal obowiązuje to po lokalnym zasięgu. Ale nie możemy zmienić faktycznego odniesienia poza pierwotnym zakresem. co to oznacza, pokazano w kolejnym przykładzie 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";
    }
}

Wynik

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

Przykład 4:

/ **

Oprócz tego, co wspomniano w przykładzie 3 (PassByValueObjectCase1.java), nie możemy zmienić faktycznego odwołania poza pierwotnym zakresem. ”

Uwaga: nie wklejam kodu dla private class Student. Definicja klasy Studentjest taka sama jak w przykładzie 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";
    }

}

Wynik

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

49

Nigdy nie można przekazać referencji w Javie, a jednym z oczywistych sposobów jest zwrócenie więcej niż jednej wartości z wywołania metody. Rozważ następujący fragment kodu w 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;
}

Czasami chcesz użyć tego samego wzorca w Javie, ale nie możesz; przynajmniej nie bezpośrednio. Zamiast tego możesz zrobić coś takiego:

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

Jak wyjaśniono w poprzednich odpowiedziach, w Javie przekazujesz wskaźnik do tablicy jako wartość do getValues. To wystarczy, ponieważ metoda następnie modyfikuje element tablicy i zgodnie z konwencją oczekujesz, że element 0 będzie zawierał wartość zwracaną. Oczywiście możesz to zrobić na inne sposoby, na przykład tworząc strukturę kodu, aby nie było to konieczne, lub konstruując klasę, która może zawierać wartość zwracaną lub pozwalać na jej ustawienie. Ale prosty wzorzec dostępny w powyższym C ++ nie jest dostępny w Javie.


48

Pomyślałem, że przekażę tę odpowiedź, aby dodać więcej szczegółów ze specyfikacji.

Po pierwsze, jaka jest różnica między przekazywaniem przez odniesienie a przekazywaniem przez wartość?

Przekazywanie przez referencję oznacza, że ​​parametr wywoływanych funkcji będzie taki sam, jak argument przekazanego argumentu (nie wartość, ale tożsamość - sama zmienna).

Przekaż przez wartość oznacza, że ​​parametr wywoływanych funkcji będzie kopią przekazanego argumentu wywołujących.

Lub z wikipedii na temat pass-by-reference

W ocenie wywołania przez referencję (zwanej także przekazywaniem przez referencję) funkcja otrzymuje niejawne odwołanie do zmiennej używanej jako argument, a nie kopii jej wartości. Zazwyczaj oznacza to, że funkcja może modyfikować (tj. Przypisywać) zmienną używaną jako argument - coś, co zobaczy osoba wywołująca.

I na temat wartości przekazywanej

W wywołaniu według wartości wyrażenie argumentu jest obliczane, a wynikowa wartość jest powiązana z odpowiednią zmienną w funkcji [...]. Jeśli funkcja lub procedura jest w stanie przypisać wartości do swoich parametrów, przypisana jest tylko jej kopia lokalna [...].

Po drugie, musimy wiedzieć, czego używa Java w swoich wywołaniach metod. Do języka Java Specification Zjednoczone

Gdy wywoływana jest metoda lub konstruktor (§15.12), wartości rzeczywistych wyrażeń argumentowych inicjują nowo utworzone zmienne parametrów , każdego z zadeklarowanego typu, przed wykonaniem treści metody lub konstruktora.

Przypisuje więc (lub wiąże) wartość argumentu do odpowiedniej zmiennej parametru.

Jaka jest wartość argumentu?

Rozważmy typy referencyjne, stany specyfikacji wirtualnej maszyny Java

Istnieją trzy rodzaje typów referencji : typy klas, typy tablic i typy interfejsów. Ich wartości są odniesieniami do dynamicznie tworzonych instancji klas, tablic lub instancji klas lub tablic, które odpowiednio implementują interfejsy.

Java Language Specification stwierdza również,

Wartości referencyjne (często tylko referencje) są wskaźnikami do tych obiektów i specjalnym zerowym odwołaniem, które nie odnosi się do żadnego obiektu.

Wartość argumentu (jakiegoś typu odwołania) jest wskaźnikiem do obiektu. Należy zauważyć, że zmienna, wywołanie metody z typem zwracanym typu odwołania i wyrażenie tworzenia instancji ( new ...) wszystkie są rozwiązywane do wartości typu odwołania.

Więc

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

Wszystkie wiążą wartość w odniesieniu do Stringprzykładu na nowo utworzonego parametru danej metody, w param. Dokładnie to opisuje definicja parametru pass-by-value. Jako taka, Java jest wartością przekazywaną .

Fakt, że możesz podążać za referencją, aby wywołać metodę lub uzyskać dostęp do pola obiektu, do którego istnieje odwołanie, jest całkowicie nieistotny dla konwersacji. Definicja przejścia przez odniesienie była następująca

Zazwyczaj oznacza to, że funkcja może modyfikować (tj. Przypisywać) zmienną używaną jako argument - coś, co zobaczy osoba wywołująca.

W Javie modyfikacja zmiennej oznacza jej ponowne przypisanie. W Javie, jeśli zmienimy przypisanie zmiennej w ramach metody, zostanie ona niezauważona dla wywołującego. Modyfikowanie obiektu, do którego odwołuje się zmienna, to zupełnie inna koncepcja.


Prymitywne wartości są również zdefiniowane w specyfikacji wirtualnej maszyny Java, tutaj . Wartością tego typu jest odpowiednia wartość całki lub zmiennoprzecinkowej, odpowiednio zakodowana (8, 16, 32, 64 itd. Bitów).


42

W Javie przekazywane są tylko referencje i wartości:

Wszystkie argumenty Java są przekazywane przez wartość (odwołanie jest kopiowane, gdy jest używane przez metodę):

W przypadku typów pierwotnych zachowanie Java jest proste: wartość jest kopiowana w innej instancji typu pierwotnego.

W przypadku obiektów jest to to samo: zmienne obiektu to wskaźniki (segmenty) zawierające tylko adres obiektu, który został utworzony przy użyciu słowa kluczowego „new” i są kopiowane jak typy pierwotne.

Zachowanie może wyglądać inaczej niż typy pierwotne: Ponieważ skopiowana zmienna obiektowa zawiera ten sam adres (do tego samego obiektu). Obiekt content / członkowie mogą nadal być modyfikowany w sposób i później dostęp do zewnątrz, dając złudzenie, że (zawierający) Przedmiot sama została przyjęta przez odniesienie.

Obiekty „String” wydają się być dobrym przykładem dla miejskiej legendy, która mówi, że „Obiekty są przekazywane przez odniesienie”:

W efekcie przy użyciu metody nigdy nie będziesz w stanie zaktualizować wartości ciągu przekazanego jako argument:

Obiekt String przechowuje znaki według tablicy zadeklarowanej jako ostateczna , której nie można modyfikować. Tylko adres obiektu może zostać zastąpiony innym przy użyciu „nowy”. Użycie „nowego” do zaktualizowania zmiennej nie pozwoli na dostęp do obiektu z zewnątrz, ponieważ zmienna została początkowo przekazana przez wartość i skopiowana.


Czyli byRef w odniesieniu do obiektów i byVal w odniesieniu do prymitywów?
Mox 18.01.17

@mox proszę przeczytać: Obiekty nie są przekazywane przez referencję, jest to ledgend: String a = new String ("niezmieniony");
user1767316

1
@Aaron „Przekaż przez referencję” nie oznacza „przekazać wartości, która jest członkiem typu, który w Javie nazywa się„ referencją ”. Dwa zastosowania „odniesienia” oznaczają różne rzeczy.
philipxy

2
Dość myląca odpowiedź. Powodem, dla którego nie można zmienić obiektu łańcucha w metodzie przekazywanej przez referencję, jest to, że obiekty String są niezmienne z założenia i nie można robić takich rzeczy strParam.setChar ( i, newValue ). To powiedziawszy, łańcuchy, jak wszystko inne, są przekazywane przez wartość, a ponieważ String jest nieprymitywnym typem, ta wartość jest odniesieniem do rzeczy, która została utworzona za pomocą new, i możesz to sprawdzić za pomocą String.intern () .
zakmck

1
Zamiast tego nie można zmienić ciągu za pomocą param = „inny ciąg” (odpowiednik nowego ciągu („inny ciąg”)), ponieważ wartość referencyjna parametru (który teraz wskazuje na „inny ciąg”) nie może wrócić z metody ciało. Ale tak jest w przypadku każdego innego obiektu, różnica polega na tym, że gdy interfejs klasy na to pozwala, możesz wykonać param.changeMe (), a obiekt najwyższego poziomu zostanie zmieniony, ponieważ parametr wskazuje na niego, pomimo samej wartości referencyjnej parametru (adresowana lokalizacja, w kategoriach C) nie może wyskoczyć z metody.
zakmck

39

Rozróżnienie, a może po prostu sposób, w jaki pamiętam, kiedy byłem pod takim samym wrażeniem jak oryginalny plakat, jest następujący: Java jest zawsze wartością. Wszystkie obiekty (w Javie, wszystko oprócz prymitywów) w Javie są referencjami. Te odniesienia są przekazywane według wartości.


2
Uważam, że twoje ostatnie zdanie jest bardzo mylące. Nie jest prawdą, że „wszystkie obiekty w Javie są referencjami”. Tylko odniesienia do tych obiektów są referencjami.
Dawood ibn Kareem

37

Jak wiele osób wspominało o tym wcześniej, Java jest zawsze wartością dodaną

Oto kolejny przykład, który pomoże ci zrozumieć różnicę ( klasyczny przykład zamiany ):

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

Wydruki:

Przed: a = 2, b = 3
Po: a = 2, b = 3

Dzieje się tak, ponieważ iA i iB są nowymi lokalnymi zmiennymi referencyjnymi, które mają tę samą wartość przekazywanych referencji (wskazują odpowiednio na a i b). Tak więc próba zmiany referencji iA lub iB zmieni się tylko w zasięgu lokalnym, a nie poza tą metodą.


32

Java przekazuje tylko wartość. Bardzo prosty przykład na sprawdzenie tego.

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
Jest to najczystszy i najprostszy sposób na sprawdzenie, czy Java przechodzi przez wartość. Przekazano wartość obj( null) init, a nie odwołanie do obj.
David Schwartz,

31

Zawsze myślę o tym jak o „przekazywaniu przez kopię”. Jest to kopia wartości pierwotnej lub referencyjnej. Jeśli jest to prymityw, jest to kopia bitów, które są wartością, a jeśli jest to obiekt, jest to kopia odwołania.

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

wyjście java PassByCopy:

name = Maxx
name = Fido

Prymitywne klasy opakowań i ciągi są niezmienne, więc każdy przykład używający tych typów nie będzie działał tak samo jak inne typy / obiekty.


5
„przechodzą przez kopię” jest to, co przechodzą przez wartość środków .
TJ Crowder,

@TJCrowder. „Przekazywanie przez kopię” może być bardziej odpowiednim sposobem wyrażenia tej samej koncepcji dla celów Java: ponieważ semantycznie bardzo trudno jest podchodzić do pomysłu podobnego do przekazywania przez referencję, co jest tym, czego chcesz w Javie.
Mike gryzoni

@mikerodent - Przepraszam, nie śledziłem tego. :-) „Pass-by-value” i „pass-by-reference” są poprawnymi warunkami dla tych pojęć. Powinniśmy ich używać (podając ich definicje, gdy ludzie ich potrzebują), zamiast tworzyć nowe. Powyżej powiedziałem, że „przekazywać przez kopię” oznacza „przekazywać przez wartość”, ale w rzeczywistości nie jest jasne, co oznacza „przekazywanie przez kopię” - co to jest kopia? Wartość? (Np. Odwołanie do obiektu.) Sam obiekt? Trzymam się więc warunków sztuki. :-)
TJ Crowder

28

W przeciwieństwie do niektórych innych języków, Java nie pozwala wybierać między wartością pass-by-referencją - wszystkie argumenty są przekazywane przez wartość. Wywołanie metody może przekazać do metody dwa typy wartości - kopie pierwotnych wartości (np. Wartości int i double) oraz kopie referencji do obiektów.

Gdy metoda modyfikuje parametr typu pierwotnego, zmiany parametru nie mają wpływu na pierwotną wartość argumentu w metodzie wywołującej.

Jeśli chodzi o obiekty, same obiekty nie mogą być przekazywane do metod. Podajemy więc referencję (adres) obiektu. Możemy manipulować oryginalnym obiektem za pomocą tego odwołania.

Jak Java tworzy i przechowuje obiekty: Kiedy tworzymy obiekt, przechowujemy adres obiektu w zmiennej referencyjnej. Przeanalizujmy następujące zdanie.

Account account1 = new Account();

„Konto konta 1” to typ i nazwa zmiennej referencyjnej, „=” to operator przypisania, „nowy” prosi o wymaganą ilość miejsca z systemu. Konstruktor po prawej stronie słowa kluczowego new, który tworzy obiekt, jest domyślnie wywoływany przez słowo kluczowe new. Adres tworzonego obiektu (wynik właściwej wartości, który jest wyrażeniem zwanym „wyrażeniem tworzenia instancji klasy”) jest przypisywany do lewej wartości (która jest zmienną referencyjną o określonej nazwie i typie) za pomocą operatora przypisania.

Chociaż referencja do obiektu jest przekazywana przez wartość, metoda może nadal oddziaływać z referencyjnym obiektem, wywołując swoje metody publiczne przy użyciu kopii referencji do obiektu. Ponieważ odwołanie przechowywane w parametrze jest kopią odwołania przekazanego jako argument, parametr w wywoływanej metodzie i argument w metodzie wywołującej odnoszą się do tego samego obiektu w pamięci.

Przekazywanie odwołań do tablic zamiast samych obiektów tablic ma sens ze względu na wydajność. Ponieważ wszystko w Javie jest przekazywane przez wartość, gdyby obiekty tablicy zostały przekazane, przekazywana byłaby kopia każdego elementu. W przypadku dużych tablic marnowałoby to czas i pochłaniało znaczne ilości miejsca na kopie elementów.

Na poniższym obrazku widać dwie zmienne referencyjne (w C / C ++ nazywane są wskaźnikami i myślę, że termin ten ułatwia zrozumienie tej funkcji) w głównej metodzie. Zmienne pierwotne i referencyjne są przechowywane w pamięci stosu (lewa strona na zdjęciach poniżej). zmienne referencyjne tablica1 i tablica2 „punkt” (jak nazywają to programiści C / C ++) lub odwołanie odpowiednio do tablic a i b, które są obiektami (wartości, które te zmienne referencyjne przechowują są adresami obiektów) w pamięci sterty (prawa strona na zdjęciach poniżej) .

Przykład podania wartości 1

Jeśli przekażemy wartość zmiennej referencyjnej tablica1 jako argument do metody reverseArray, w metodzie tworzona jest zmienna referencyjna i ta zmienna referencyjna zaczyna wskazywać na tę samą tablicę (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);
    }
}

Przykład podania wartości 2

Więc jeśli powiemy

array1[0] = 5;

w metodzie reverseArray spowoduje zmianę w tablicy a.

Mamy inną zmienną referencyjną w metodzie reverseArray (array2), która wskazuje na tablicę c. Gdybyśmy mieli powiedzieć

array1 = array2;

w metodzie reverseArray, wówczas zmienna referencyjna tablica1 w metodzie reverseArray przestaje wskazywać na tablicę a i zaczyna wskazywać na tablicę c (linia przerywana na drugim obrazie).

Jeśli zwrócimy wartość zmiennej referencyjnej tablica 2 jako wartość zwracaną metody reverseArray i przypiszemy tę wartość do zmiennej referencyjnej tablica 1 w metodzie głównej, tablica 1 w metodzie zacznie wskazywać na tablicę c.

Napiszmy więc wszystkie rzeczy, które zrobiliśmy teraz.

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

wprowadź opis zdjęcia tutaj

A teraz, gdy metoda reverseArray się skończyła, zmienne referencyjne (tablica1 i tablica2) zniknęły. Co oznacza, że ​​mamy teraz tylko dwie zmienne referencyjne w głównej tablicy metod1 i tablicy2, które wskazują odpowiednio na tablice c i b. Żadna zmienna odniesienia nie wskazuje na obiekt (tablicę) a. Jest więc uprawniony do zbierania śmieci.

Możesz także przypisać wartość tablicy 2 głównej do tablicy 1. tablica 1 zaczyna wskazywać na b.


27

Stworzyłem wątek poświęcony tego rodzaju pytań do jakichkolwiek języków programowania tutaj .

Wspomniana jest również Java . Oto krótkie podsumowanie:

  • Java przekazuje parametry według wartości
  • „według wartości” to jedyny sposób w java na przekazanie parametru do metody
  • użycie metod z obiektu podanego jako parametr zmieni obiekt, ponieważ odniesienia wskazują na oryginalne obiekty. (jeśli ta metoda sama zmienia niektóre wartości)

27

Krótko mówiąc, obiekty Java mają bardzo szczególne właściwości.

W ogóle, Java ma prymitywne typy ( int, bool, char, double, etc), które są przekazywane bezpośrednio przez wartość. Następnie Java ma obiekty (wszystko, co pochodzijava.lang.Object ). Obiekty są zawsze obsługiwane przez odwołanie (odwołanie jest wskaźnikiem, którego nie można dotknąć). Oznacza to, że w rzeczywistości obiekty są przekazywane przez odniesienie, ponieważ odniesienia zwykle nie są interesujące. Oznacza to jednak, że nie można zmienić wskazanego obiektu, ponieważ samo odwołanie jest przekazywane przez wartość.

Czy to brzmi dziwnie i myląco? Zastanówmy się, jak C implementuje przekazywanie przez referencję i przekazywanie przez wartość. W C domyślną konwencją jest przekazywanie wartości. void foo(int x)przekazuje wartość int według wartości. void foo(int *x)Jest to funkcja, która nie chce int a, ale wskaźnik do int: foo(&a). Można by tego użyć z &operatorem, aby przekazać zmienny adres.

Weź to do C ++, a mamy referencje. Odniesienia są zasadniczo (w tym kontekście) cukrem syntaktycznym, który ukrywa wskaźnikową część równania: void foo(int &x)jest wywoływany przez foo(a), gdzie sam kompilator wie, że jest to odwołanie i anależy podać adres braku odniesienia . W Javie wszystkie zmienne odnoszące się do obiektów są w rzeczywistości typu referencyjnego, w efekcie wymuszając wywołanie przez referencję dla większości celów i celów bez drobnoziarnistej kontroli (i złożoności) zapewnianej na przykład przez C ++.


Myślę, że jest to bardzo bliskie mojemu zrozumieniu obiektu Java i jego odniesienia. Obiekt w Javie jest przekazywany do metody przez kopię referencyjną (lub alias).
MaxZoom,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.