我的一个学生在使用三元运算符时会得到空指针异常,有时会导致null。我想我理解这个问题,但似乎是因为类型推断不一致所致。换句话说,我觉得这里的语义不一致,错误应该可以避免,而不改变他的方法。
此问题类似于Another question about ternary operators,但与{{3}}不同。在那个问题中,null Integer必须强制为int,因为函数的返回值是int。但是,我学生的代码并非如此。
此代码运行良好:
Integer x = (5>7) ? 3 : null;
x的值为null。没有NPE。在这种情况下,编译器可以确定三元运算符的结果需要是Integer,因此它将3(一个int)转换为Integer而不是将null转换为int。
但是,运行此代码:
Integer x = (5>7) ? 3 : (5 > 8) ? 4 : null;
导致NPE。发生这种情况的唯一原因是因为null被转换为int,但这并不是必需的,并且似乎与第一位代码不一致。也就是说,如果编译器可以为第一个snipet推断出三元运算符的结果是整数,为什么不能在第二种情况下这样做呢?第二个三元表达式的结果必须是整数,并且因为该结果是第一个三元运算符的第二个结果,所以第一个三元运算符的结果也应该是整数。
另一个snipet正常工作:
Integer three = 3;
Integer x = (5>7) ? three : (5 > 8) ? three+1 : null;
在这里,编译器似乎能够推断出两个三元运算符的结果都是一个整数,所以不强制将null转换为int。
答案 0 :(得分:11)
关键是条件运算符是右关联的。确定条件表达式的结果类型的规则是hideously complicated,但归结为:
(5 > 8) ? 4 : null
,第二个操作数是int
,第三个是null
,如果我们在表中查找,则此表达式的结果类型为{{ 1}}。 (换句话说:因为其中一个操作数为Integer
,因此将其视为参考条件表达式) null
进行评估,这意味着在上面链接的表中,我们需要查找第二个操作数(5>7) ? 3 : <previous result>
和第三个操作数int
的结果类型:它是{ {1}}。这意味着Integer
需要取消装箱,并且失败并显示int
。那么为什么第一种情况会起作用呢?
我们已经有了<previous result>
,正如我们所看到的,第二个操作数是NPE
而第三个是(5>7) ? 3 : null;
,结果类型是int
。但我们将其分配给null
类型的变量,因此不需要拆箱。
但是这只发生在Integer
文字中,以下代码仍会抛出NPE,因为操作数类型Integer
和null
会产生数字条件表达式强>:
int
总结一下:它有一种逻辑,但它不是人的逻辑。
Integer
,则结果为Integer i = null;
Integer x = (5>7) ? 3 : i;
。Integer
而另一个操作数为Integer
,则结果为int
。Integer
引用),则结果为int
。答案 1 :(得分:1)
int x = (int) (5>7) ? 3 : null;
导致NPE,因为null被转换为int。
与您的代码相同
Integer x = (5>7) ? 3 : (5 > 8) ? 4 : null;
看起来像这样:
Integer x = (Integer) ((5>7) ? (int) 3 : (5 > 8) ? 4 : null);
如您所见,null再次转换为int,失败。
要解决这个问题,可以这样做:
Integer x = (Integer) ((5>7) ? (Integer) 3 : (5 > 8) ? 4 : null);
现在它不会尝试将null转换为int,而是整数。
答案 2 :(得分:0)
在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
都是&#34;显然&#34;原始类型(或盒装版本),它被视为独立的。 Quoting丹史密斯 -
我们在这里对条件表达式进行分类,以增强引用条件(15.25.3)的类型规则,同时保留布尔值和数值条件的现有行为。如果我们尝试统一处理所有条件,则会出现各种不必要的不兼容更改,包括重载决策和装箱/拆箱行为的更改。
在你的情况下
Integer x = false ? 3 : false ? 4 : null;
因为false?4:null
显然&#34;(?)和Integer
,所以父表达式为?:int:Integer
;这是一个原始的案例,它的行为与java7保持兼容,因此,NPE。
我在#34;明确&#34;因为这是我直觉的理解;我不确定正式的规格。让我们来看看这个例子
static <T> T f1(){ return null; }
--
Integer x = false ? 3 : false ? f1() : null;
它汇编!并且在运行时没有NPE!我不知道如何遵循这个案例的规范。我可以想象编译器可能会执行以下步骤:
1)子表达式false?f1():null
不是&#34;显然&#34; a(盒装)原始类型;它的类型尚不清楚
2)因此,父表达式被分类为&#34;参考条件表达式&#34;,它出现在赋值上下文中。
3)目标类型Integer
应用于操作数,最终应用于f1()
,然后推断为返回Integer
?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里面发生了一些非常时髦的事情。