请考虑以下代码
public class JDK10Test {
public static void main(String[] args) {
Double d = false ? 1.0 : new HashMap<String, Double>().get("1");
System.out.println(d);
}
}
在JDK8上运行时,此代码打印为null,而在JDK10上,此代码导致NullPointerException
Exception in thread "main" java.lang.NullPointerException
at JDK10Test.main(JDK10Test.java:5)
除了JDK10编译器生成的两个与自动装箱相关的附加指令外,编译器生成的字节码几乎完全相同,并且似乎负责NPE.
15: invokevirtual #7 // Method java/lang/Double.doubleValue:()D
18: invokestatic #8 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
这种行为是JDK10中的错误还是故意更改以使行为更严格?
JDK8: java version "1.8.0_172"
JDK10: java version "10.0.1" 2018-04-17
解决方法:
我相信这是一个似乎已经解决的错误.根据JLS,抛出NullPointerException似乎是正确的行为.
我认为这里发生的是由于某些原因在版本8中,编译器考虑了方法的返回类型提到的类型变量的边界而不是实际的类型参数.换句话说,它认为… get(“1”)返回Object.这可能是因为它正在考虑方法的擦除或其他原因.
该行为应取决于get方法的返回类型,如下面的§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
.
换句话说,如果两个表达式都可以转换为数字类型,并且一个是基本的而另一个是盒装的,则三元条件的结果类型是基本类型.
(表15.25-C还方便地向我们展示了三元表达式boolean的类型?double:Double确实是双倍的,再次意味着拆箱和投掷是正确的.)
如果get方法的返回类型不可转换为数字类型,则三元条件将被视为“引用条件表达式”,并且不会发生拆箱.
另外,我认为注释“对于泛型方法,这是在实例化方法的类型参数之前的类型”不应该适用于我们的情况. Map.get没有声明类型变量,so it’s not a generic method by the JLS’ definition.但是,这个注释是在Java 9中添加的(唯一的变化,see JLS8),因此它可能与我们今天看到的行为有关.
对于HashMap< String,Double>,get的返回类型应为Double.
这是一个支持我的理论的MCVE,编译器正在考虑类型变量边界而不是实际的类型参数:
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是:
a == null
java.lang.NullPointerException
换句话说,尽管e.nullAsNumber()和e.nullAsDouble()具有相同的实际返回类型,但只有e.nullAsDouble()被视为“数字表达式”.方法之间的唯一区别是类型变量绑定.
可能会有更多的调查,但我想发布我的发现.我尝试了很多东西,发现只有当表达式是返回类型中带有类型变量的方法时,才会发生错误(即没有取消装箱/ NPE).
有趣的是,我在Java 8中发现了the following program also throws:
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"));
}
}
这表明编译器的行为实际上是不同的,这取决于三元表达式是分配给局部变量还是方法参数.
(最初我想使用重载来证明编译器给三元表达式赋予的实际类型,但鉴于上述差异,它看起来不太可能.有可能还有另一种我没有想到的方法,虽然.)