why entre diferencias java java-8 javac unboxing java-10

entre - java 9 or 10



Diferencia en el comportamiento del operador ternario en JDK8 y JDK10 (2)

Considere el siguiente código

public class JDK10Test { public static void main(String[] args) { Double d = false ? 1.0 : new HashMap<String, Double>().get("1"); System.out.println(d); } }

Cuando se ejecuta en JDK8, este código se imprime en null mientras que en JDK10 este código da como resultado la excepción NullPointerException

Exception in thread "main" java.lang.NullPointerException at JDK10Test.main(JDK10Test.java:5)

El bytecode producido por los compiladores es casi idéntico, aparte de dos instrucciones adicionales producidas por el compilador JDK10 que están relacionadas con el autoboxing y parecen ser responsables de la NPE.

15: invokevirtual #7 // Method java/lang/Double.doubleValue:()D 18: invokestatic #8 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;

¿Es este comportamiento un error en JDK10 o un cambio intencional para hacer que el comportamiento sea más estricto?

JDK8: java version "1.8.0_172" JDK10: java version "10.0.1" 2018-04-17


Creo que este fue un error que parece haber sido corregido. El lanzamiento de una NullPointerException parece ser el comportamiento correcto, de acuerdo con el JLS.

Creo que lo que está sucediendo aquí es que, por alguna razón, en la versión 8, el compilador consideró los límites de la variable de tipo mencionada por el tipo de retorno del método en lugar de los argumentos de tipo reales. En otras palabras, piensa que ...get("1") devuelve Object . Esto podría deberse a que está considerando la eliminación del método, o alguna otra razón.

El comportamiento debe depender del tipo de retorno del método de get , como se especifica en los siguientes extractos de §15.26 :

  • Si las expresiones del segundo y tercer operando son expresiones numéricas , la expresión condicional es una expresión condicional numérica.

    Para el propósito de clasificar un condicional, las siguientes expresiones son expresiones numéricas:

    • […]

    • Una expresión de invocación de método (§15.12) para la cual el método más específico elegido (§15.12.2.5) tiene un tipo de retorno que se puede convertir a un tipo numérico.

      Tenga en cuenta que, para un método genérico, este es el tipo antes de crear una instancia de los argumentos de tipo del método.

    • […]

  • De lo contrario, la expresión condicional es una expresión condicional de referencia.

[…]

El tipo de una expresión condicional numérica se determina de la siguiente manera:

  • […]

  • Si uno de los operandos segundo y tercero es de tipo primitivo T , y el tipo del otro es el resultado de aplicar la conversión de boxeo (§5.1.7) a T , entonces el tipo de la expresión condicional es T

En otras palabras, si ambas expresiones son convertibles a un tipo numérico, y una es primitiva y la otra está encuadrada, entonces el tipo de resultado del condicional ternario es el tipo primitivo.

(La Tabla 15.25-C también nos muestra convenientemente que el tipo de expresión ternaria boolean ? double : Double sería double , lo que significa que deshacer y lanzar es correcto).

Si el tipo de retorno del método de get no fuera convertible a un tipo numérico, entonces el condicional ternario se consideraría una "expresión condicional de referencia" y no se produciría la eliminación de la caja.

Además, creo que la nota "para un método genérico, este es el tipo antes de crear una instancia de los argumentos de tipo del método" no debería aplicarse a nuestro caso. Map.get no declara variables de tipo, por lo que no es un método genérico según la definición de JLS . Sin embargo, esta nota se agregó en Java 9 (siendo el único cambio, consulte JLS8 ), por lo que es posible que tenga algo que ver con el comportamiento que estamos viendo hoy.

Para un HashMap<String, Double> , el tipo de retorno de get debería ser Double .

Aquí hay un MCVE que apoya mi teoría de que el compilador está considerando los límites de la variable de tipo en lugar de los argumentos de tipo reales:

class Example<N extends Number, D extends Double> { N nullAsNumber() { return null; } D nullAsDouble() { return null; } public static void main(String[] args) { Example<Double, Double> e = new Example<>(); try { Double a = false ? 0.0 : e.nullAsNumber(); System.out.printf("a == %f%n", a); Double b = false ? 0.0 : e.nullAsDouble(); System.out.printf("b == %f%n", b); } catch (NullPointerException x) { System.out.println(x); } } }

La salida de ese programa en Java 8 es:

a == null java.lang.NullPointerException

En otras palabras, a pesar de que e.nullAsNumber() y e.nullAsDouble() tienen el mismo tipo de retorno real, solo e.nullAsDouble() se considera como una "expresión numérica". La única diferencia entre los métodos es la variable de tipo enlazada.

Probablemente haya más investigación que se pueda hacer, pero quería publicar mis conclusiones. Intenté bastantes cosas y descubrí que el error (es decir, no unboxing / NPE) parece ocurrir solo cuando la expresión es un método con una variable de tipo en el tipo de retorno.

Curiosamente, he encontrado que el siguiente programa también lanza en Java 8:

import java.util.*; class Example { static void accept(Double d) {} public static void main(String[] args) { accept(false ? 1.0 : new HashMap<String, Double>().get("1")); } }

Eso muestra que el comportamiento del compilador es realmente diferente, dependiendo de si la expresión ternaria se asigna a una variable local o un parámetro del método.

(Originalmente, quería usar sobrecargas para demostrar el tipo real que el compilador está dando a la expresión ternaria, pero no parece que sea posible dada la diferencia anterior. Es posible que haya otra forma en la que no haya pensado, aunque.)


JLS 10 no parece especificar ningún cambio en el operador condicional, pero tengo una teoría.

De acuerdo con JLS 8 y JLS 10, si la segunda expresión ( 1.0 ) es de tipo double y la tercera ( new HashMap<String, Double>().get("1") ) es de tipo Double , entonces el resultado de La expresión condicional es de tipo double . La JVM en Java 8 parece ser lo suficientemente inteligente como para saber que, debido a que está devolviendo un Double , no hay razón para desempaquetar primero el resultado de HashMap#get a un double y luego encajarlo de nuevo en un Double (porque especificó Double ).

Para probar esto, cambie Double a double en su ejemplo, y se NullPointerException una NullPointerException (en JDK 8); esto se debe a que ahora está ocurriendo el null.doubleValue() , y null.doubleValue() obviamente lanza una NullPointerException .

double d = false ? 1.0 : new HashMap<String, Double>().get("1"); System.out.println(d); // Throws a NullPointerException

Parece que esto cambió en 10, pero no puedo decir por qué.