Difference in behaviour of the ternary operator on JDK8 and JDK10
I believe this was a bug which seems to have been fixed. Throwing a NullPointerException
seems to be the correct behavior, according to the JLS.
I think that what is going on here is that for some reason in version 8, the compiler considered the bounds of the type variable mentioned by the method's return type rather than the actual type arguments. In other words, it thinks ...get("1")
returns Object
. This could be because it's considering the method's erasure, or some other reason.
The behavior should hinge upon the return type of the get
method, as specified by the below excerpts from §15.26:
If both the second and the third operand expressions are numeric expressions, the conditional expression is a numeric conditional expression.
For the purpose of classifying a conditional, the following expressions are numeric expressions:
[…]
A method invocation expression (§15.12) for which the chosen most specific method (§15.12.2.5) has a return type that is convertible to a numeric type.
Note that, for a generic method, this is the type before instantiating the method's type arguments.
[…]
Otherwise, the conditional expression is a reference conditional expression.
[…]
The type of a numeric conditional expression is determined as follows:
[…]
If one of the second and third operands is of primitive type
T
, and the type of the other is the result of applying boxing conversion (§5.1.7) toT
, then the type of the conditional expression isT
.
In other words, if both expressions are convertible to a numeric type, and one is primitive and the other is boxed, then the result type of the ternary conditional is the primitive type.
(Table 15.25-C also conveniently shows us that the type of a ternary expression boolean ? double : Double
would indeed be double
, again meaning unboxing and throwing is correct.)
If the return type of the get
method wasn't convertible to a numeric type, then the ternary conditional would be considered a "reference conditional expression" and unboxing wouldn't occur.
Also, I think the note "for a generic method, this is the type before instantiating the method's type arguments" shouldn't apply to our case. Map.get
doesn't declare type variables, so it's not a generic method by the JLS' definition. However, this note was added in Java 9 (being the only change, see JLS8), so it's possible that it has something to do with the behavior we're seeing today.
For a HashMap<String, Double>
, the return type of get
should be Double
.
Here's an MCVE supporting my theory that the compiler is considering the type variable bounds rather than the actual type arguments:
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);
}
}
}
The output of that program on Java 8 is:
a == null
java.lang.NullPointerException
In other words, despite e.nullAsNumber()
and e.nullAsDouble()
having the same actual return type, only e.nullAsDouble()
is considered as a "numeric expression". The only difference between the methods is the type variable bound.
There's probably more investigation that could be done, but I wanted to post my findings. I tried quite a few things and found that the bug (i.e. no unboxing/NPE) seems to only happen when the expression is a method with a type variable in the return type.
Interestingly, I've found that the following program also throws in 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"));
}
}
That shows that the compiler's behavior is actually different, depending on whether the ternary expression is assigned to a local variable or a method parameter.
(Originally I wanted to use overloads to prove the actual type that the compiler is giving to the ternary expression, but it doesn't look like that's possible given the above difference. It's possible there's still another way that I haven't thought of, though.)
JLS 10 doesn't seem to specify any changes to the conditional operator, but I have a theory.
According to JLS 8 and JLS 10, if the second expression (1.0
) is of type double
and the third (new HashMap<String, Double>().get("1")
) is of type Double
, then the result of the conditional expression is of type double
. The JVM in Java 8 seems to be smart enough to know that, because you're returning a Double
, there's no reason to first unbox the result of HashMap#get
to a double
and then box it back to a Double
(because you specified Double
).
To prove this, change Double
to double
in your example, and a NullPointerException
is thrown (in JDK 8); this is because the unboxing is now occuring, and null.doubleValue()
obviously throws a NullPointerException
.
double d = false ? 1.0 : new HashMap<String, Double>().get("1");
System.out.println(d); // Throws a NullPointerException
It seems that this was changed in 10, but I can't tell you why.