2015-07-26 23 views
6

Ten fragment koduWyjaśnienie odniesienia i metody wykonania StringBuilder celu

StringBuilder b1=new StringBuilder("hello"); 
    b1.append(b1.append("!")); 
    System.out.println("b1 = "+b1); 

wypisze

b1 = hello!hello! 

ponieważ wewnętrzna append wykonywany jest pierwszy i modyfikuje obiektu b1; następnie oceniana jest zewnętrzna wartość b1 (jest ona teraz równa hello!) i dołączany jest do niej ten sam ciąg. Więc

  1. wewnętrzny wyrażenie jest wykonywane
  2. oryginalny obiekt zostanie zmodyfikowany
  3. ekspresja zewnętrzna wykonywana jest na zmodyfikowanej obiektu

Ale teraz, dlaczego ten kod rzucać NullPointerException?

StringBuilder s1=null; 
    StringBuilder s2=new StringBuilder("world"); 
    try{s1.append(s1=s2.append("!"));} 
    catch(Exception e){System.out.println(e);} 
    System.out.println("s1 = "+s1+"\ns2 = "+s2+"\n"); 

i drukuje

java.lang.NullPointerException 
    s1 = world! 
    s2 = world! 

Spodziewałem odniesienie s1 być wskazując na obiekt odwołuje s2przed zewnętrzna append zostanie wykonany.

W pewnym sensie przypisanie b1.append("!"); wpływa na "zewnętrzny" b1, ale s1=s2.append("!") nie. Wiem, że wynika to z faktu, że w pierwszym przypadku modyfikuję obiekt, podczas gdy w drugim modyfikuję odwołanie, ale ... w jakiej kolejności wartości/referencje/metody są oceniane i wykonywane?

Edit

samo dzieje się z tablicami:

int[] y = { 0, 0, 0 }; 
    try {y[y[0] = 2] = 4;} 
    catch (Exception e) {System.out.println(e);} 
    System.out.println("y = "+Arrays.toString(y)+"\n"); 

wydruków

y = [2, 0, 4] 

podczas

int[] x1 = null; 
    int[] x2 = { 1, 2, 3 }; 
    try {x1[(x1=x2)[0]] = 0;} 
    catch (Exception e) {System.out.println(e);} 
    System.out.println("x1 = "+Arrays.toString(x1)+"\nx2 = "+Arrays.toString(x2)); 

drukuje

Odpowiedz

2

ta jest określona w JLS 15.12.4 .:

Jeżeli forma jest ExpressionName.[TypeArguments] Identifier, a następnie:

  • Jeśli tryb wywołania jest statyczny, wówczas nie ma odniesienia docelowego. Wyrażenie ExpressionName jest oceniane, ale wynik zostaje odrzucony jako .

  • W przeciwnym razie odniesienie docelowe jest wartością oznaczoną przez ExpressionName.

i

W ramach wywołania metody, przykładowo (§15.12) jest wyrazem oznaczająca przedmiotu powoływać. To wyrażenie wydaje się być w pełni wyliczone zanim zostanie oszacowana jakakolwiek część dowolnego argumentu wywołania metody.

Więc w linii s1.append(s1=s2.append("!"));s1 (przed .append(s1 = ...)) oceniana jest najpierw przed wyrażeniem argumentu s1=s2.append("!"). Tak więc odniesienie null jest pamiętane jako odniesienie docelowe przed zmianą s1 w celu odniesienia do instancji StringBuilder s2.

Następnie wyrażenie argumentu jest obliczane, aby wykonać polecenie s1=s2.append("!"). Pamiętał jednak wcześniej odniesienie do celu, więc metoda append jest wywoływana na odwołaniu null, a wynik wywołania generuje NullPointerException.

+0

Ok, ale tutaj "b1.append (b1.append ("! "));" Spodziewałbym się, że zewnętrzny "b1" zostanie oceniony jako równy "hello", a następnie wykona wewnętrzne polecenie i spowoduje "hellohello! ' –

+1

@LuigiCortese' b1.append ("!"); 'Nie zwraca nowej instancji. Więc odniesienie jest wciąż takie samo. –

+0

Uhm ... i dlaczego robi różnicę ...? Mam na myśli, jeśli reguła jest taka, że ​​* odniesienie zewnętrzne jest oceniane przed wyrażeniem wewnętrznym *, nie rozumiem, dlaczego nie mogłem mieć 'hellohello!'. Dość zdezorientowany! –

0

w b1.append(b1.append("!")); nie wewnętrzny załącznik jest wykonywany jako pierwszy. Java wywoła pierwszy załącznik, a następnie oceni parametr b1.append("!") dla tego połączenia, które zmodyfikuje obiekt b1. W tym przypadku metoda s1.append(s1=s2.append("!")); zadzwoni pod numer s1.append(), ale ponieważ s1 ma wartość null, pojawi się NullPointerException.

+0

w drugim przykładzie 's1 = s2.append ("!")' Jest faktycznie wykonywany i modyfikuje przedmiotu oraz odniesienie 's1' –

+0

ale w tym momencie, gdy wywołasz 's1.append()' 's1' jest pusty – Jorj

+0

Oczywiście. Moje pytanie brzmi: jeśli 's1 = s2.append ("! ")' Zostanie wykonane przed 's1.append()', dlaczego w tej drugiej instrukcji 's1' jest puste? –

1

Rzućmy okiem na kod bajtowy w przykładzie,

0: aconst_null 
    1: astore_1 
    // Comment: null is stored to s1. 
    2: new   #18     // class java/lang/StringBuilder 
    5: dup 
    6: ldc   #20     // String world 
    8: invokespecial #22     // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V 
    11: astore_2 
    // Comment: new StringBuilder is stored to s2. 
    12: aload_1 
    // Comment: s1 (which is null) is loaded for method call. 
    13: aload_2 
    // Comment: s2 is loaded for method call. 
    14: ldc   #25     // String ! 
    16: invokevirtual #27     // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 
    19: dup 
    20: astore_1 
    // Comment: s2.append() return value is stored in s1. 
    21: invokevirtual #31     // Method java/lang/StringBuilder.append:(Ljava/lang/CharSequence;)Ljava/lang/StringBuilder; 
    // Comment: append() method is called on already loaded s1 value (which is null). 
    24: pop 
    25: return 

Jeśli czytasz w moich komentarzach w kodzie, będziesz wiedzieć, że null jest ładowany przez wywołanie metody append().

Weźmy inny przykład,

StringBuilder s1 = new StringBuilder(); 
    StringBuilder s2 = new StringBuilder("world"); 
    s1.append(s1 = s2.append("!")); 
    System.out.println(s1); 

To będzie drukować tylko world!. Nawet jeśli można się spodziewać world!world!.

Dzieje się tak, ponieważ ponownie wczytujesz wartość s1 po załadowaniu do wywołania metody. Co oznacza, że ​​w wywołaniu metody ponownie przypisana wartość zostanie nadpisana.

1

Co się dzieje, że interpreter Javy najpierw próbuje zlokalizować (nie oceniać, zlokalizować) metodę, w tym przypadku s1.append(). Domyślam się, że robi to, aby dodać wskaźnik metody do stosu. Aby to zrobić, musi znać dokładną klasę obiektu s1, więc usuwa ją. Ponieważ s1 ma wartość null, wynikiem jest NullPointerException.

Zdarza się to, zanim argumenty zostaną ocenione, stąd fakt, że s1 nadal jest null.

This SO answer wymienia różne etapy, które występują w naszym s1.append rozmowy:

  1. Wskaźnik obiekt jest wykorzystywany do odniesienia do obiektu, a stamtąd obiektu klasy.

  2. Wskaźnik metody znajduje się w obiekcie klasy. (Przeszukiwanie do konwersji nazwy metody do indeksu metody zostało w dużej mierze wykonane po załadowaniu klasy , więc jest to w zasadzie tylko operacja indeksu tablicy.)

  3. Generalnie jakiś rodzaj "znaku" jest przesyłany na stos JVM . Zawierałoby to wskaźnik instrukcji dzwoniącego i wskaźnik do bazy swojego stosu. (Wiele różnych implementacji tutaj.)

  4. Ta definicja metody jest sprawdzana, aby zobaczyć, ile jest wymaganych lokalnych zmiennych. Tak wiele pustych elementów jest popychanych na stos.

  5. Wskaźnik obiektu ("ten") jest przechowywany w lokalnym var 0, a wszelkie pliki parm są przechowywane w 1,2,3 ... odpowiednio.

  6. Kontrola jest przekazywana do wywoływanej metody.

NullPointerException występuje w kroku 1.