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) aT
, entonces el tipo de la expresión condicional esT
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é.