我相信这是一个似乎已修复的错误。
NullPointerException根据JLS的说法,抛出a 似乎是正确的行为。
我认为这里发生的是由于版本8中的某种原因,编译器考虑了方法的返回类型而不是实际类型参数所提及的类型变量的范围。换句话说,它认为
...get("1")returnObject。这可能是因为它正在考虑该方法的擦除或其他一些原因。
行为应取决于
get方法的返回类型,如以下第15.26节摘录所指定:
- 如果第二和第三操作数表达式都是 数字 表达式,则条件表达式是数字条件表达式。
为了对条件进行分类,以下表达式是数字表达式:
* […]* **方法调用表达式(第15.12节),为其选择的最特定的方法(第15.12.2.5节)具有可转换为数字类型的返回类型。**请注意,对于泛型方法,这是实例化方法的类型参数之前的类型。
* […]
- 否则,条件表达式是参考条件表达式。
[…]
数字条件表达式的类型确定如下:
[…]
如果第二个和第三个操作数之一是原始类型
T,而另一个操作数的类型是将装箱转换(第5.1.7节)应用于的结果T,则条件表达式的类型为T。
换句话说,如果两个表达式都可转换为数字类型,并且一个表达式是原始类型,而另一个则被装箱,则三元条件的结果类型将是原始类型。
(表15.25-C还方便地向我们显示了三元表达式的类型
boolean ? double :Double的确是
double,再次表示拆箱和投掷是正确的。)
如果该
get方法的返回类型不能转换为数字类型,则三元条件将被视为“引用条件表达式”,并且不会发生拆箱。
另外,我认为注释 “对于通用方法,这是在实例化方法的类型参数之前的类型”
不适用于我们的情况。
Map.get没有声明类型变量,因此它不是JLS定义的通用方法。但是,此注释
是 在Java 9 中
添加的(是唯一的更改,请参阅JLS8),因此它可能与我们今天看到的行为有关。
对于a
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); } }}该程序在Java 8上的输出为:
a == nulljava.lang.NullPointerException
换句话说,尽管
e.nullAsNumber()与
e.nullAsDouble()具有相同的实际返回类型,只有
e.nullAsDouble()被认为是一个“数字表达”。方法之间的唯一区别是类型变量绑定。
可能还有更多的调查可以做,但是我想发表我的发现。我尝试了很多事情,发现该错误(即没有取消装箱/ NPE)似乎仅在表达式是带有返回类型的类型变量的方法时发生。
有趣的是,我发现以下程序也在 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")); }}这表明编译器的行为实际上是不同的,具体取决于将三元表达式分配给局部变量还是方法参数。
(本来我想使用重载来证明编译器为三元表达式提供的实际类型,但是鉴于上述差异,这似乎不太可能。我可能还没有想到另一种方法,虽然。)



