Dlaczego to nie zgłasza wyjątku NullPointerException?

89

Nead wyjaśnienie dla następującego kodu:

StringBuilder sample = new StringBuilder();
StringBuilder referToSample = sample;
referToSample.append("B");
System.out.println(sample);

To zostanie wydrukowane w Btaki sposób, że dowody samplei referToSampleobiekty odnoszą się do tego samego odniesienia do pamięci.

StringBuilder sample = new StringBuilder();
StringBuilder referToSample = sample;
sample.append("A");
referToSample.append("B");
System.out.println(referToSample);

Spowoduje to wydrukowanie, ABktóre również dowodzi tego samego.

StringBuilder sample = new StringBuilder();
StringBuilder referToSample = sample;
referToSample = null;
referToSample.append("A");
System.out.println(sample);

Oczywiście to się wyrzuci, NullPointerExceptionponieważ próbuję wywołać appendodwołanie zerowe.

StringBuilder sample = new StringBuilder();
StringBuilder referToSample = sample;
referToSample = null;
sample.append("A");
System.out.println(sample);

Więc oto moje pytanie, dlaczego ostatni przykład kodu nie jest rzucany, NullPointerExceptionponieważ to, co widzę i rozumiem z pierwszych dwóch przykładów, to jeśli dwa obiekty odnoszą się do tego samego obiektu, to jeśli zmienimy jakąkolwiek wartość, to również odbije się to na innej, ponieważ oba wskazują to samo odniesienie do pamięci. Dlaczego więc ta zasada nie ma tutaj zastosowania? Jeśli przypiszę nulldo referToSample, sample również powinno mieć wartość null i powinno zgłaszać wyjątek NullPointerException, ale go nie zgłasza, dlaczego?

popełnić
źródło
31
samplejest nadal sample. Tylko się zmieniłeś referToSample.
Dave Newton,
25
Głosowane / oznaczone gwiazdkami! Bardzo podstawowe pytanie, ale jest to piękny przykład wyjaśniający problem i dobrze zadający pytanie.
Ryan Ransford,
15
Jedna kwestia terminologiczna w twoim pytaniu: ciągle odnosisz się do obiektówsample i referToSamplejako obiekty, ale nie są one obiektami, są zmiennymi. Zmienna może zawierać odniesienie do obiektu, ale sama nie jest obiektem. To subtelne rozróżnienie, ale w zasadzie jest to esencja twojego pomieszania.
Daniel Pryden,
1
Pomaga myśleć o zmiennych obiektowych jako tylko wskaźnikach. Każdy podmiot, który działa na zmiennej ( volatile, final, =, ==...), gdy stosuje się do zmiennej obiektu wpływa na wskaźnik , a nie odnosi się do obiektu.
MikeFHay,
2
@Arpit Z całym szacunkiem się nie zgadzam. W tym pytaniu kryje się wielka koncepcja, a mianowicie różnica między przedmiotem a odniesieniem do tego obiektu. W większości przypadków nie musimy (ani nie chcemy) być świadomi tej różnicy, a projektanci języka ciężko pracują, aby to przed nami ukryć. Wystarczy pomyśleć na przykład o argumentach przekazywanych przez odwołanie w C ++. Nie jest więc dla mnie zaskoczeniem, widząc początkujących zdezorientowanych całą tą magią!
Gyom

Odpowiedzi:

89

nullprzypisania nie zmieniają wartości poprzez globalne niszczenie tego obiektu. Takie zachowanie prowadziłoby do trudnych do śledzenia błędów i sprzecznych z intuicją zachowań. Łamią tylko to konkretne odniesienie .

Dla uproszczenia powiedzmy, że samplewskazuje na adres 12345. Prawdopodobnie nie jest to adres i jest używany tylko dla uproszczenia. Adres jest zwykle reprezentowany za pomocą dziwnego szesnastkowego podanego w Object#hashCode(), ale jest to zależne od implementacji. 1

StringBuilder sample = new StringBuilder(); //sample refers to 
//StringBuilder at 12345 

StringBuilder referToSample = sample; //referToSample refers to 
//the same StringBuilder at 12345 
//SEE DIAGRAM 1

referToSample = null; //referToSample NOW refers to 00000, 
//so accessing it will throw a NPE. 
//The other reference is not affected.
//SEE DIAGRAM 2

sample.append("A"); //sample STILL refers to the same StringBuilder at 12345 
System.out.println(sample);

Z zaznaczonych linii See diagramschematy obiektów w tym czasie przedstawiają się następująco:

Schemat 1:

[StringBuilder sample]    -----------------> [java.lang.StringBuilder@00012345]
                                                      ↑
[StringBuilder referToSample] ------------------------/

Schemat 2:

[StringBuilder sample]    -----------------> [java.lang.StringBuilder@00012345]

[StringBuilder referToSample] ---->> [null pointer]

Diagram 2 pokazuje, że anulowanie referToSamplenie przerywa odwołania sampledo StringBuilder w 00012345.

1 Rozważania GC sprawiają, że jest to nieprawdopodobne.

nanofarad
źródło
@commit, jeśli to odpowiada na Twoje pytanie, kliknij zaznaczenie poniżej strzałek głosowania po lewej stronie powyższego tekstu.
Ray Britton,
@RayBritton Uwielbiam to, jak ludzie zakładają, że najwyższa głosowana odpowiedź to ta, która koniecznie odpowiada na pytanie. Chociaż liczba ta jest ważna, nie jest to jedyna metryka; Odpowiedzi -1 i -2 mogą zostać zaakceptowane tylko dlatego, że najbardziej pomogły PO.
nanofarad,
11
hashCode()to nie to samo, co adres pamięci
Kevin Panko
6
Tożsamość hashCode jest zwykle uzyskiwana z lokalizacji pamięci Object przy pierwszym wywołaniu metody. Od tego momentu ten hashCode jest ustalany i zapamiętywany, nawet jeśli menedżer pamięci zdecyduje się przenieść obiekt do innej lokalizacji pamięci.
Holger,
3
Klasy, które przesłaniają, hashCodezazwyczaj definiują ją jako zwracającą wartość, która nie ma nic wspólnego z adresami pamięci. Myślę, że bez wspominania o tym można poruszyć kwestię odniesień do obiektów i obiektów hashCode. Dokładniejsze informacje, jak uzyskać adres pamięci, można zostawić na inny dzień.
Kevin Panko,
62

Początkowo była jak mówiłeś referToSamplemiał na myśli sample, jak pokazano poniżej:

1. Scenariusz 1:

referToSample odnosi się do sample

2. Scenariusz 1 (ciąg dalszy):

referToSample.append ("B")

  • Tutaj jak się referToSampleodnosiłeś sample, więc podczas pisania zostało dodane „B”

    referToSample.append("B")

To samo stało się w Scenariuszu 2:

Ale w 3. Scenariuszu 3: jak powiedział hexafraction,

po przypisaniu nulldo referToSamplekiedy odnosił samplesię nie zmienił wartości , zamiast po prostu łamie odwołanie od sample, a teraz wskazuje nigdzie . jak pokazano niżej:

kiedy referToSample = null

Teraz, ponieważ nigdzie niereferToSample wskazuje , więc podczas gdy ty nie miałbyś żadnych wartości ani odniesień, w których może dołączyć A. Więc to rzuci .referToSample.append("A");NullPointerException

ALE samplejest nadal taki sam, jak zainicjowałeś go

StringBuilder sample = new StringBuilder(); więc został zainicjalizowany, więc teraz może dołączyć A i nie będzie rzucał NullPointerException

Parth
źródło
5
Ładne diagramy. Pomaga to zilustrować.
nanofarad
ładny diagram, ale notatka na dole powinna brzmieć „Teraz referToSample nie odnosi się do„ ””, ponieważ kiedy odnosisz referToSample = sample;się do próbki, po prostu kopiujesz adres tego, do czego się odnosi
Khaled.K
17

W skrócie: przypisujesz wartość null zmiennej referencyjnej, a nie obiektowi.

W jednym przykładzie zmieniasz stan obiektu, do którego odwołują się dwie zmienne odniesienia. W takim przypadku obie zmienne odniesienia będą odzwierciedlać zmianę.

W innym przykładzie zmieniasz odniesienie przypisane do jednej zmiennej, ale nie ma to wpływu na sam obiekt, więc druga zmienna, która nadal odwołuje się do oryginalnego obiektu, nie zauważy żadnej zmiany stanu obiektu.


Jeśli chodzi o Twoje szczegółowe „zasady”:

jeśli dwa obiekty odnoszą się do tego samego obiektu, to jeśli zmienimy jakąkolwiek wartość, będzie to również odzwierciedlać inne, ponieważ oba wskazują na to samo odniesienie do pamięci.

Ponownie odnosisz się do zmiany stanu jednego obiektu, do którego odnoszą się obie zmienne .

Dlaczego więc ta zasada nie ma tutaj zastosowania? Jeśli przypiszę null do referToSample, to sample również powinno mieć wartość null i powinno zgłaszać nullPointerException, ale nie jest rzucane, dlaczego?

Ponownie, zmieniasz referencję jednej zmiennej, która nie ma absolutnie żadnego wpływu na referencję drugiej zmiennej.

Są to dwie zupełnie różne czynności i skutkują dwoma zupełnie różnymi rezultatami.

Poduszkowiec pełen węgorzy
źródło
3
@commit: jest to podstawowa, ale krytyczna koncepcja, która leży u podstaw całej Javy i której, gdy już ją zobaczysz, nigdy nie zapomnisz.
Poduszkowiec pełen węgorzy
8

Zobacz ten prosty diagram:

diagram

Kiedy można nazwać metodę na referToSample, a następnie [your object]jest aktualizowany, dzięki czemu wpływa sampleteż. Ale kiedy mówisz referToSample = null, po prostu zmieniasz to, do czego się referToSample odnosi .

tckmn
źródło
3

Tutaj „sample” i „referToSample” odnoszą się do tego samego obiektu. To jest koncepcja różnych wskaźników uzyskujących dostęp do tego samego miejsca w pamięci. Zatem przypisanie jednej zmiennej referencyjnej do wartości null nie niszczy obiektu.

   referToSample = null;

oznacza, że ​​„referToSample” wskazuje tylko na wartość null, obiekt pozostaje taki sam, a inne zmienne odniesienia działają dobrze. Tak więc dla „próbki”, która nie wskazuje na wartość null i ma prawidłowy obiekt

   sample.append("A");

działa w porządku. Ale jeśli spróbujemy dodać null do 'referToSample', wyświetli się NullPointException. To jest,

   referToSample .append("A");-------> NullPointerException

Dlatego w trzecim fragmencie kodu masz wyjątek NullPointerException.

NCA
źródło
0

Za każdym razem, gdy używane jest nowe słowo kluczowe, tworzy ono obiekt na stercie

1) StringBuilder sample = new StringBuilder ();

2) StringBuilder referToSample = sample;

W 2) referencja referSample jest tworzona na tej samej próbce obiektu

zatem referToSample = null; ma wartość Nulling Tylko odwołanie referSample nie daje żadnego wpływu na sample dlatego nie otrzymujesz wyjątku wskaźnika NULL Dzięki funkcji Garbage Collection w Javie

nitesh
źródło
0

Po prostu proste, Java nie ma przekazywania przez referencję, po prostu przekazuje referencję do obiektu.

Peerapat A
źródło
2
Semantyka przekazywania parametrów tak naprawdę nie ma nic wspólnego z opisaną sytuacją.
Michael Myers