Java三元运算符似乎不一致地将整数强制转换为整数

时间:2015-10-16 18:27:39

标签: java casting

我的一个学生在使用三元运算符时会得到空指针异常,有时会导致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。

3 个答案:

答案 0 :(得分:11)

关键是条件运算符是右关联的。确定条件表达式的结果类型的规则是hideously complicated,但归结为:

  1. 评估第一个(5 > 8) ? 4 : null,第二个操作数是int,第三个是null,如果我们在表中查找,则此表达式的结果类型为{{ 1}}。 (换句话说:因为其中一个操作数为Integer,因此将其视为参考条件表达式
  2. 然后我们要null进行评估,这意味着在上面链接的表中,我们需要查找第二个操作数(5>7) ? 3 : <previous result>和第三个操作数int的结果类型:它是{ {1}}。这意味着Integer需要取消装箱,并且失败并显示int
  3. 那么为什么第一种情况会起作用呢?

    我们已经有了<previous result>,正如我们所看到的,第二个操作数是NPE而第三个是(5>7) ? 3 : null;,结果类型是int。但我们将其分配给null类型的变量,因此不需要拆箱。

    但是这只发生在Integer文字中,以下代码仍会抛出NPE,因为操作数类型Integernull会产生数字条件表达式

    int

    总结一下:它有一种逻辑,但它不是人的逻辑。

    1. 如果两个操作数都是编译类型Integer,则结果为Integer i = null; Integer x = (5>7) ? 3 : i;
    2. 如果一个操作数的类型为Integer而另一个操作数为Integer,则结果为int
    3. 如果一个操作数属于null type(其唯一有效值为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中,如果op2op3都是&#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里面发生了一些非常时髦的事情。

(另见Java autoboxing and ternary operator madness