Boolesche Werte, bedingte Operatoren und Autoboxing

132

Warum wirft das 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;
}

während dies nicht tut

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

?

Die Lösung besteht im Übrigen darin , sie zu ersetzen, falseum Boolean.FALSEzu vermeiden null, dass sie booleanentpackt wird - was nicht möglich ist. Das ist aber nicht die Frage. Die Frage ist warum ? Gibt es in JLS Referenzen, die dieses Verhalten bestätigen, insbesondere im zweiten Fall?

BalusC
quelle
28
Wow, Autoboxing ist eine endlose Quelle von ... ähm ... Überraschungen für den Java-Programmierer, nicht wahr? :-)
Leonbloy
Ich hatte ein ähnliches Problem und was mich überraschte war, dass es auf der OpenJDK-VM fehlschlug, aber auf der HotSpot-VM funktionierte ... Einmal schreiben, überall ausführen!
Kodu

Antworten:

92

Der Unterschied besteht darin, dass der explizite Typ der returnsNull()Methode die statische Typisierung der Ausdrücke zur Kompilierungszeit beeinflusst:

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

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

Siehe Java-Sprachspezifikation, Abschnitt 15.25 Bedingter Operator? ::

  • Für E1, die Typen der 2. und 3. Operanden sind Booleanund booleanjeweils, so dass diese Klausel gilt:

    Wenn einer der zweiten und dritten Operanden vom Typ Boolean und der Typ des anderen vom Typ Boolean ist, ist der Typ des bedingten Ausdrucks boolean.

    Da der Typ des Ausdrucks ist boolean, muss der 2. Operand dazu gezwungen werden boolean. Der Compiler fügt Auto-Unboxing-Code in den 2. Operanden (Rückgabewert von returnsNull()) ein, um ihn zum Typ zu machen boolean. Dies führt natürlich dazu, dass die NPE nullzur Laufzeit zurückgegeben wird.

  • Für E2 sind die Typen des 2. und 3. Operanden <special null type>(nicht Booleanwie in E1!) Und booleandaher gilt keine spezifische Typisierungsklausel ( lesen Sie sie! ), Daher gilt die letzte "ansonsten" -Klausel:

    Ansonsten sind der zweite und der dritte Operand vom Typ S1 bzw. S2. Sei T1 der Typ, der sich aus der Anwendung der Boxkonvertierung auf S1 ergibt, und sei T2 der Typ, der sich aus der Anwendung der Boxkonvertierung auf S2 ergibt. Der Typ des bedingten Ausdrucks ergibt sich aus der Anwendung der Capture-Konvertierung (§5.1.10) auf lub (T1, T2) (§15.12.2.7).

    • S1 == <special null type>(siehe §4.1 )
    • S2 == boolean
    • T1 == box (S1) == <special null type>(siehe letzten Punkt in der Liste der Boxumwandlungen in §5.1.7 )
    • T2 == box (S2) == `Boolean
    • lub (T1, T2) == Boolean

    Der Typ des bedingten Ausdrucks ist also Booleanund der 3. Operand muss dazu gezwungen werden Boolean. Der Compiler fügt Auto-Boxing-Code für den 3. Operanden ( false) ein. Der 2. Operand benötigt nicht das automatische Entpacken wie in E1, daher wird kein NPE für das automatische Entpacken nullzurückgegeben.


Diese Frage erfordert eine ähnliche Typanalyse:

Java-Bedingungsoperator ?: Ergebnistyp

Bert F.
quelle
4
Macht Sinn ... denke ich. Der §15.12.2.7 ist ein Schmerz.
BalusC
Es ist einfach ... aber nur im Nachhinein. :-)
Bert F
@BertF Was bedeutet die Funktion lubin lub(T1,T2)Standplatz?
Geek
1
@Geek - lub () - kleinste Obergrenze - im Grunde die nächste Superklasse, die sie gemeinsam haben; Da null (Typ "der spezielle Null-Typ") implizit in einen beliebigen Typ konvertiert (erweitert) werden kann, können Sie den speziellen Null-Typ für die Zwecke von lub () als "Superklasse" eines beliebigen Typs (einer Klasse) betrachten.
Bert F
25

Die Linie:

    Boolean b = true ? returnsNull() : false;

wird intern umgewandelt in:

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

das Unboxing durchführen; also: null.booleanValue()ergibt eine NPE

Dies ist eine der größten Gefahren bei der Verwendung von Autoboxing. Dieses Verhalten ist in der Tat in 5.1.8 JLS dokumentiert

Bearbeiten: Ich glaube, das Unboxing ist darauf zurückzuführen, dass der dritte Operator vom booleschen Typ ist, wie (implizite Umwandlung hinzugefügt):

   Boolean b = (Boolean) true ? true : false; 
jjungnickel
quelle
2
Warum versucht es so zu entpacken, wenn der endgültige Wert ein Boolesches Objekt ist?
Erick Robertson
16

Aus der Java-Sprachspezifikation, Abschnitt 15.25 :

  • Wenn einer der zweiten und dritten Operanden vom Typ Boolean und der Typ des anderen vom Typ Boolean ist, ist der Typ des bedingten Ausdrucks boolean.

So ist das erste Beispiel versucht anzurufen , Boolean.booleanValue()um zu konvertieren , Booleanum booleangemäß der ersten Regel.

Im zweiten Fall ist der erste Operand vom Nulltyp, wenn der zweite nicht vom Referenztyp ist, sodass die Autoboxing-Konvertierung angewendet wird:

  • Ansonsten sind der zweite und der dritte Operand vom Typ S1 bzw. S2. Sei T1 der Typ, der sich aus der Anwendung der Boxkonvertierung auf S1 ergibt, und sei T2 der Typ, der sich aus der Anwendung der Boxkonvertierung auf S2 ergibt. Der Typ des bedingten Ausdrucks ergibt sich aus der Anwendung der Capture-Konvertierung (§5.1.10) auf lub (T1, T2) (§15.12.2.7).
axtavt
quelle
Dies beantwortet den ersten Fall, aber nicht den zweiten Fall.
BalusC
Wahrscheinlich gibt es eine Ausnahme, wenn einer der Werte ist null.
Erick Robertson
@Erick: Bestätigt JLS dies?
BalusC
1
@Erick: Ich denke nicht, dass es anwendbar ist, da booleanes kein Referenztyp ist.
Axtavt
1
Und darf ich hinzufügen ... aus diesem Grund sollten Sie beide Seiten eines Ternärs vom gleichen Typ machen, ggf. mit expliziten Aufrufen. Selbst wenn Sie die Spezifikationen gespeichert haben und wissen, was passieren wird, kann es sein, dass der nächste Programmierer, der Ihren Code liest, dies nicht tut. Meiner bescheidenen Meinung nach wäre es besser, wenn der Compiler in diesen Situationen nur eine Fehlermeldung ausgeben würde, als Dinge zu tun, die für gewöhnliche Sterbliche schwer vorherzusagen sind. Nun, vielleicht gibt es Fälle, in denen das Verhalten wirklich nützlich ist, aber ich habe noch keinen getroffen.
Jay
0

Wir können dieses Problem anhand des Bytecodes erkennen. In Zeile 3 des Hauptbytecodes 3: invokevirtual #3 // Method java/lang/Boolean.booleanValue:()Z, dem Box-Booleschen Wert Null, invokevirtualder Methode java.lang.Boolean.booleanValue, wird natürlich NPE ausgelöst.

    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
quelle