Booleanos, operadores condicionales y autoboxing

132

¿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 falsepor Boolean.FALSEpara evitar nullser desempaquetado en, booleanlo 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?

BalusC
fuente
28
wow, el autoboxing es una fuente interminable de ... er ... sorpresas para el programador de Java, ¿no? :-)
leonbloy
Tuve un problema similar y lo que me sorprendió fue que falló en OpenJDK VM pero funcionó en HotSpot VM ... ¡Escriba una vez, ejecute en cualquier lugar!
kodu

Respuestas:

92

La diferencia es que el tipo explícito del returnsNull()método afecta el tipeo estático de las expresiones en tiempo de compilació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 segundo y tercero son Booleany booleanrespectivamente, por lo que se aplica esta cláusula:

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

    Como el tipo de expresión es boolean, el segundo operando debe ser forzado boolean. El compilador inserta el código de desempaquetado automático en el segundo operando (valor de retorno de returnsNull()) para que escriba boolean. Por supuesto, esto hace que el NPE se nulldevuelva en tiempo de ejecución.

  • Para E2, los tipos de los operandos segundo y tercero son <special null type>(¡no Booleancomo en E1!) Y booleanrespectivamente, por lo que no se aplica una cláusula de escritura específica (¡ vaya a leerlos! ), Por lo que se aplica la cláusula final "de lo contrario":

    De lo contrario, el segundo y tercer operandos son de los tipos S1 y S2 respectivamente. Deje que T1 sea el tipo que resulta de aplicar la conversión de boxeo a S1, y que T2 sea el 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 == <special null type>(ver §4.1 )
    • S2 == boolean
    • T1 == box (S1) == <special null type>(ver el último elemento en la lista de conversiones de boxeo en §5.1.7 )
    • T2 == box (S2) == `Boolean
    • lubricante (T1, T2) == Boolean

    Entonces, el tipo de expresión condicional es Booleany el tercer operando debe ser forzado Boolean. El compilador inserta el código de auto-boxeo para el tercer operando ( false). El segundo operando no necesita el desempaquetado automático como en E1, por lo que no se requiere un NPE de desempaquetado automático cuando nullse devuelve.


Esta pregunta necesita un análisis de tipo similar:

Operador condicional de Java?: Tipo de resultado

Bert F
fuente
44
Tiene sentido ... creo. El §15.12.2.7 es un dolor.
BalusC
Es fácil ... pero solo en retrospectiva. :-)
Bert F
@BertF ¿Qué significa la función luben lub(T1,T2)?
Geek
1
@Geek - lub () - límite superior mínimo - básicamente la superclase más cercana que tienen en común; dado que null (tipo "el tipo nulo especial") puede convertirse (ensancharse) implícitamente a cualquier tipo, puede considerar que el tipo nulo especial es una "superclase" de cualquier tipo (clase) a los efectos de lub ().
Bert F
25

La línea:

    Boolean b = true ? returnsNull() : false;

se transforma internamente en:

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

para realizar el unboxing; así: null.booleanValue()producirá un NPE

Esta es una de las principales dificultades al usar el autoboxing. Este comportamiento está documentado en 5.1.8 JLS

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

   Boolean b = (Boolean) true ? true : false; 
jjungnickel
fuente
2
¿Por qué trata de desempaquetar así, cuando el valor final es un objeto booleano?
Erick Robertson
16

De Java Language Specification, sección 15.25 :

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

Entonces, el primer ejemplo intenta llamar Boolean.booleanValue()para convertir Booleana booleansegú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 autoboxing:

  • De lo contrario, el segundo y tercer operandos son de los tipos S1 y S2 respectivamente. Deje que T1 sea el tipo que resulta de aplicar la conversión de boxeo a S1, y que T2 sea el 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).
axtavt
fuente
Esto responde al primer caso, pero no al segundo caso.
BalusC
Probablemente hay una excepción para cuando uno de los valores es null.
Erick Robertson
@Erick: ¿JLS confirma esto?
BalusC
1
@Erick: No creo que sea aplicable ya booleanque no es un tipo de referencia.
axtavt
1
Y puedo agregar ... es por eso que debes hacer que ambos lados de un ternario sean del mismo tipo, con llamadas explícitas si es necesario. Incluso si tiene las especificaciones memorizadas y sabe lo que sucederá, es posible que el próximo programador que venga y lea su código no lo haga. En mi humilde opinión, sería mejor si el compilador solo produjera un mensaje de error en estas situaciones en lugar de hacer cosas que son difíciles de predecir para los mortales comunes. Bueno, tal vez hay casos en los que el comportamiento es realmente útil, pero aún no he tocado uno.
Jay
0

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, invokevirtualel 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
Yanhui Zhou
fuente