Booleanos, operadores condicionales y autoboxing

¿Por qué esto arroja 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; } 

mientras esto no

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

?

La solución es, por cierto, reemplazar false por Boolean.FALSE para evitar que null sea ​​unboxed a boolean lo que no es posible. Pero esa no es la pregunta. La pregunta es por qué ? ¿Hay alguna referencia en JLS que confirme este comportamiento, especialmente del segundo caso?

La diferencia es que el tipo explícito del método returnsNull() afecta al tipado estático de las expresiones en tiempo de comstackción:

 E1: `true ? returnsNull() : false` - boolean (auto-unboxing 2nd operand to boolean) E2: `true ? null : false` - Boolean (autoboxing of 3rd operand to Boolean) 

Consulte Especificación del lenguaje Java, sección 15.25 Operador condicional? :

  • Para E1, los tipos de los operandos 2º y 3º son Boolean y boolean respectivamente, por lo que esta cláusula se aplica:

    Si uno de los operandos segundo y tercero es de tipo booleano y el tipo de otro es de tipo booleano, entonces el tipo de expresión condicional es booleano.

    Como el tipo de expresión es boolean , el 2º operando debe ser forzado a boolean . El comstackdor inserta el código de auto-unboxing en el 2do operando (valor de retorno de returnsNull() ) para hacer que escriba boolean . Esto, por supuesto, hace que el NPE del null devuelto en el tiempo de ejecución.

  • Para E2, los tipos de los operandos 2 y 3 son (¡no Boolean como en E1!) Y boolean respectivamente, por lo que no se aplica una cláusula de tipaje específica ( ¡léalo! ), Por lo que se aplica la cláusula final “de otro modo” :

    De lo contrario, el segundo y tercer operandos son de tipos S1 y S2, respectivamente. Deje que T1 sea del tipo que resulta de aplicar la conversión de boxeo a S1, y deje que T2 sea del tipo que resulta de aplicar la conversión de boxeo a S2. El tipo de expresión condicional es el resultado de aplicar la conversión de captura (§5.1.10) a lub (T1, T2) (§15.12.2.7).

    • S1 == (ver §4.1 )
    • S2 == boolean
    • T1 == box (S1) == (ver último elemento en la lista de conversiones de boxeo en §5.1.7 )
    • T2 == box (S2) == `Booleano
    • lub (T1, T2) == Boolean

    Entonces, el tipo de expresión condicional es Boolean y el 3er operando debe ser forzado a Boolean . El comstackdor inserta el código de auto-boxing para el 3er operando ( false ). El segundo operando no necesita el auto-unboxing como en E1 , por lo que no se desempaqueta automáticamente NPE cuando se devuelve null .


Esta pregunta necesita un análisis de tipo similar:

¿Operador condicional Java?: Tipo de resultado

La línea:

  Boolean b = true ? returnsNull() : false; 

se transforma internamente a:

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

para realizar el desempaquetado; así: null.getBoolean() producirá un NPE

Este es uno de los principales escollos al usar el autoboxing. Este comportamiento está de hecho documentado en 5.1.8 JLS

Editar: creo que el desempaquetado se debe a que el tercer operador es de tipo booleano, como (agregado implícito):

  Boolean b = (Boolean) true ? true : false; 

De Java Language Specification, sección 15.25 :

  • Si uno de los operandos segundo y tercero es de tipo booleano y el tipo de otro es de tipo booleano, entonces el tipo de expresión condicional es booleano.

Entonces, el primer ejemplo intenta llamar a Boolean.booleanValue() para convertir Boolean a boolean según la primera regla.

En el segundo caso, el primer operando es del tipo nulo, cuando el segundo no es del tipo de referencia, por lo que se aplica la conversión de autoboxeo:

  • De lo contrario, el segundo y tercer operandos son de tipos S1 y S2, respectivamente. Deje que T1 sea del tipo que resulta de aplicar la conversión de boxeo a S1, y deje que T2 sea del tipo que resulta de aplicar la conversión de boxeo a S2. El tipo de expresión condicional es el resultado de aplicar la conversión de captura (§5.1.10) a lub (T1, T2) (§15.12.2.7).

Podemos ver este problema desde el código de bytes. En la línea 3 del código de byte principal, 3: invokevirtual #3 // Method java/lang/Boolean.booleanValue:()Z , el boxeo booleano de valor nulo, invokevirtual el método java.lang.Boolean.booleanValue , arrojará NPE por supuesto.

  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