Wartości logiczne, operatory warunkowe i autoboxing

133

Dlaczego to rzuca NullPointerException

public static void main(String[] args) throws Exception {
    Boolean b = true ? returnsNull() : false; // NPE on this line.
    System.out.println(b);
}

public static Boolean returnsNull() {
    return null;
}

podczas gdy to nie jest

public static void main(String[] args) throws Exception {
    Boolean b = true ? null : false;
    System.out.println(b); // null
}

?

Rozwiązaniem jest przy okazji zastąpienie falseprzez, Boolean.FALSEaby uniknąć nullrozpakowania do boolean- co nie jest możliwe. Ale to nie jest pytanie. Pytanie brzmi: dlaczego ? Czy są jakieś odniesienia w JLS, które potwierdzają to zachowanie, szczególnie w przypadku drugiego przypadku?

BalusC
źródło
28
wow, autoboxing jest niekończącym się źródłem ... eee ... niespodzianek dla programisty Java, prawda? :-)
leonbloy
Miałem podobny problem i zaskoczyło mnie to, że nie udało się na VM OpenJDK, ale działało na HotSpot VM ... Napisz raz, uruchom gdziekolwiek!
kod

Odpowiedzi:

92

Różnica polega na tym, że jawny typ returnsNull()metody wpływa na statyczne typowanie wyrażeń w czasie kompilacji:

E1: `true ? returnsNull() : false` - boolean (auto-unboxing 2nd operand to boolean)

E2: `true ? null : false` - Boolean (autoboxing of 3rd operand to Boolean)

Patrz specyfikacja języka Java, sekcja 15.25 Operator warunkowy? :

  • Dla E1 typy drugiego i trzeciego operandu to odpowiednio Booleani boolean, więc obowiązuje ta klauzula:

    Jeśli jeden z operandów drugiego i trzeciego jest typu boolowskiego, a typ drugiego jest typu boolowskiego, to wyrażenie warunkowe jest typu boolowskiego.

    Ponieważ typ wyrażenia to boolean, drugi operand musi zostać wymuszony boolean. Kompilator wstawia kod automatycznego rozpakowywania do drugiego operandu (wartość zwracana returnsNull()), aby nadać mu typ boolean. To oczywiście powoduje, że NPE jest nullzwracane w czasie wykonywania.

  • W przypadku E2 typy drugiego i trzeciego operandu to <special null type>(inne Booleanniż w E1!) I booleanodpowiednio, więc nie ma zastosowania żadna określona klauzula typowania ( przeczytaj je! ), Więc obowiązuje ostatnia klauzula „inaczej”:

    W przeciwnym razie drugi i trzeci operand są odpowiednio typu S1 i S2. Niech T1 będzie typem wynikającym z zastosowania konwersji pudełkowej do S1 i niech T2 będzie typem, który wynika z zastosowania konwersji pudełkowej do S2. Typ wyrażenia warunkowego jest wynikiem zastosowania konwersji przechwytywania (§5.1.10) do lub (T1, T2) (§15.12.2.7).

    • S1 == <special null type>(patrz §4.1 )
    • S2 == boolean
    • T1 == box (S1) == <special null type>(patrz ostatnia pozycja na liście konwersji bokserskich w §5.1.7 )
    • T2 == box (S2) == `Boolean
    • lub (T1, T2) == Boolean

    Tak więc typ wyrażenia warunkowego to, Booleana trzeci operand musi zostać wymuszony Boolean. Kompilator wstawia kod auto-boxingu dla trzeciego operandu ( false). Drugi operand nie wymaga automatycznego rozpakowywania, jak w E1, więc nie ma automatycznego rozpakowywania NPE, gdy nulljest zwracany.


To pytanie wymaga podobnej analizy typu:

Operator warunkowy Java?: Typ wyniku

Bert F.
źródło
4
To ma sens ... myślę. §15.12.2.7 jest ból.
BalusC
To łatwe ... ale tylko z perspektywy czasu. :-)
Bert F
@BertF Co funkcję lubw lub(T1,T2)skrót?
Geek
1
@Geek - lub () - najmniejsza górna granica - w zasadzie najbliższa nadklasa, którą mają wspólną; ponieważ null (typ „specjalny typ null”) może być niejawnie konwertowany (rozszerzany) do dowolnego typu, można uznać specjalny typ zerowy za „nadklasę” dowolnego typu (klasy) na potrzeby funkcji lub ().
Bert F
26

Linia:

    Boolean b = true ? returnsNull() : false;

jest wewnętrznie przekształcany w:

    Boolean b = true ? returnsNull().booleanValue() : false; 

do wykonania rozpakowywania; w ten sposób: null.booleanValue()przyniesie NPE

Jest to jedna z głównych pułapek podczas korzystania z autoboxingu. To zachowanie jest rzeczywiście udokumentowane w 5.1.8 JLS

Edycja: Uważam, że rozpakowywanie jest spowodowane tym, że trzeci operator jest typu boolowskiego, na przykład (dodano niejawne rzutowanie):

   Boolean b = (Boolean) true ? true : false; 
jjungnickel
źródło
2
Dlaczego próbuje tak rozpakować, skoro ostateczną wartością jest obiekt boolowski?
Erick Robertson
16

Ze specyfikacji języka Java, sekcja 15.25 :

  • Jeśli jeden z operandów drugiego i trzeciego jest typu boolowskiego, a typ drugiego jest typu boolowskiego, to wyrażenie warunkowe jest typu boolowskiego.

Tak więc, pierwszym przykładzie próbuje wywołać Boolean.booleanValue()w celu przekształcenia Booleansię booleanjak na pierwszej reguły.

W drugim przypadku pierwszy operand jest typu null, gdy drugi nie jest typu referencyjnego, więc stosowana jest konwersja autoboxu:

  • W przeciwnym razie drugi i trzeci operand są odpowiednio typu S1 i S2. Niech T1 będzie typem wynikającym z zastosowania konwersji pudełkowej do S1 i niech T2 będzie typem, który wynika z zastosowania konwersji pudełkowej do S2. Typ wyrażenia warunkowego jest wynikiem zastosowania konwersji przechwytywania (§5.1.10) do lub (T1, T2) (§15.12.2.7).
axtavt
źródło
Odpowiada to na pierwszy przypadek, ale nie na drugi przypadek.
BalusC
Prawdopodobnie istnieje wyjątek, gdy jedna z wartości to null.
Erick Robertson
@Erick: czy JLS to potwierdza?
BalusC
1
@Erick: Nie sądzę, że ma to zastosowanie, ponieważ booleannie jest typem referencyjnym.
axtavt
1
I mogę dodać ... dlatego powinieneś uczynić obie strony trójskładnika tego samego typu, z wyraźnymi wywołaniami, jeśli to konieczne. Nawet jeśli masz zapamiętane specyfikacje i wiesz, co się stanie, następny programista, który przyjdzie i przeczyta Twój kod, może nie. Moim skromnym zdaniem byłoby lepiej, gdyby kompilator po prostu wygenerował komunikat o błędzie w takich sytuacjach, zamiast robić rzeczy, które są trudne do przewidzenia przez zwykłych śmiertelników. Cóż, może są przypadki, w których zachowanie jest naprawdę przydatne, ale jeszcze żadnego nie trafiłem.
Jay
0

Możemy zobaczyć ten problem z kodu bajtowego. W linii 3 kodu bajtowego maina, 3: invokevirtual #3 // Method java/lang/Boolean.booleanValue:()Zboxingu Boolean o wartości null, invokevirtualmetoda java.lang.Boolean.booleanValue, oczywiście wyrzuci NPE.

    public static void main(java.lang.String[]) throws java.lang.Exception;
      descriptor: ([Ljava/lang/String;)V
      flags: ACC_PUBLIC, ACC_STATIC
      Code:
        stack=2, locals=2, args_size=1
           0: invokestatic  #2                  // Method returnsNull:()Ljava/lang/Boolean;
           3: invokevirtual #3                  // Method java/lang/Boolean.booleanValue:()Z
           6: invokestatic  #4                  // Method java/lang/Boolean.valueOf:(Z)Ljava/lang/Boolean;
           9: astore_1
          10: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
          13: aload_1
          14: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
          17: return
        LineNumberTable:
          line 3: 0
          line 4: 10
          line 5: 17
      Exceptions:
        throws java.lang.Exception

    public static java.lang.Boolean returnsNull();
      descriptor: ()Ljava/lang/Boolean;
      flags: ACC_PUBLIC, ACC_STATIC
      Code:
        stack=1, locals=0, args_size=0
           0: aconst_null
           1: areturn
        LineNumberTable:
          line 8: 0
Yanhui Zhou
źródło