在 Java8 之前,几乎在所有情况下†,表达式的类型都是自下而上构建的,完全取决于子表达式的类型;它不依赖于上下文。这很好,很简单,代码也很容易理解;例如,重载解析取决于参数的类型,这些参数的解析独立于方法调用上下文。
(†我知道的唯一例外是jls#15.12.2.8)
给定一个?int:Integer形式的条件表达式,规范需要为它定义一个固定的类型,而不考虑上下文。选择了 int 类型,这在大多数用例中可能更好。
当然,这也是NPE从拆箱的来源。
在 Java8 中,上下文类型信息可以用于类型推断。这在很多情况下都很方便;但它也引入了混淆,因为可能有两个方向来解析表达式的类型。幸运的是,有些表达式仍然是独立的;它们的类型与上下文无关。
w.r.t 条件表达式,我们不希望像false?0:1 这样的简单表达式依赖于上下文;他们的类型是不言而喻的。另一方面,我们确实希望对更复杂的条件表达式进行上下文类型推断,例如false?f():g(),其中f/g() 需要类型推断。
在原始类型和引用类型之间划清界限。在op1?op2:op3 中,如果op2 和op3 都是“明显”的原始类型(或盒装版本),则将其视为独立的。 Quoting丹·史密斯-
我们在这里对条件表达式进行分类是为了增强参考条件 (15.25.3) 的键入规则,同时保留布尔和数字条件的现有行为。如果我们尝试统一处理所有条件,则会出现各种不想要的不兼容更改,包括重载分辨率和装箱/拆箱行为的更改。
你的情况
Integer x = false ? 3 : false ? 4 : null;
由于false?4:null 是“显然”(?) 一个Integer,父表达式的形式为?:int:Integer;这是一个原始案例,它的行为与 java7 保持兼容,因此是 NPE。
我在“清楚地”上加上引号是因为这是我的直觉理解;我不确定正式规格。我们来看这个例子
static <T> T f1(){ return null; }
--
Integer x = false ? 3 : false ? f1() : null;
它编译!并且运行时没有 NPE!我不知道如何遵循这个案例的规范。我可以想象编译器可能会执行以下步骤:
1) 子表达式false?f1():null 不是“明确”的(盒装)原始类型;它的类型尚不清楚
2) 因此,父表达式被归类为“引用条件表达式”,它出现在赋值上下文中。
3) 目标类型Integer 应用于操作数,最终应用于f1(),然后推断返回Integer
4) 但是,我们现在不能返回将条件表达式重新分类为?int:Integer。
这听起来很合理。但是,如果我们明确指定 f1() 的类型参数呢?
Integer x = false ? 3 : false ? Test.<Integer>f1() : null;
理论 (A) - 这不应该改变程序的语义,因为它与推断的类型参数相同。我们不应该在运行时看到 NPE。
理论(B)——没有类型推断;子表达式的类型显然是Integer,因此这应该归类为原始大小写,我们应该在运行时看到NPE。
我相信(B);但是,javac(8u60) 会执行 (A)。我不明白为什么。
把这个观察推到一个有趣的水平
class MyList1 extends ArrayList<Integer>
{
//inherit public Integer get(int index)
}
class MyList2 extends ArrayList<Integer>
{
@Override public Integer get(int index)
{
return super.get(0);
}
}
MyList1 myList1 = new MyList1();
MyList2 myList2 = new MyList2();
Integer x1 = false ? 3 : false ? myList1.get(0) : null; // no NPE
Integer x2 = false ? 3 : false ? myList2.get(0) : null; // NPE !!!
这没有任何意义; javac 内部发生了一些非常时髦的事情。
(另见Java autoboxing and ternary operator madness)