【问题标题】:Java Ternary Operator seems to be inconsistantly casting Integers to intsJava 三元运算符似乎一直在将整数转换为 int
【发布时间】:2016-01-15 14:33:33
【问题描述】:

我的一个学生在使用有时会导致 null 的三元运算符时遇到空指针异常。我想我理解这个问题,但它似乎是由不一致的类型推断引起的。或者换一种说法,我觉得这里的语义不一致,在不改变他的方法的情况下应该可以避免错误。

这个问题与Another question about ternary operators 相似但不同。在那个问题中,必须将 null Integer 强制为 int,因为函数的返回值是 int。但是,我学生的代码中并非如此。

这段代码运行良好:

Integer x = (5>7) ? 3 : null;

x 的值为空。没有 NPE。在这种情况下,编译器可以判断出三元运算符的结果需要是 Integer,因此它将 3(一个 int)转换为 Integer,而不是将 null 转换为 int。

但是,运行这段代码:

Integer x = (5>7) ? 3 : (5 > 8) ? 4 : null;

导致 NPE。发生这种情况的唯一原因是因为 null 被强制转换为 int,但这并不是真正必要的,而且似乎与第一段代码不一致。也就是说,如果编译器可以为第一个片段推断出三元运算符的结果是一个整数,为什么它不能在第二种情况下这样做呢?第二个三元表达式的结果必须是整数,并且由于该结果是第一个三元运算符的第二个结果,所以第一个三元运算符的结果也应该是一个整数。

另一个片段可以正常工作:

Integer three = 3;

Integer x = (5>7) ? three : (5 > 8) ? three+1 : null;

在这里,编译器似乎能够推断出两个三元运算符的结果都是整数,因此不会强制将 null 强制转换为 int。

【问题讨论】:

标签: java casting


【解决方案1】:

在 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 都是“明显”的原始类型(或盒装版本),则将其视为独立的。 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

【讨论】:

  • 经验教训 - 在 op2op3 中始终使用明确的相同类型。永远不要在这里混用intInteger;如有必要,进行手动显式装箱/拆箱。永远不要在这里混用 intlong 等;如有必要,进行手动原始类型转换。这部分规范太不可靠了。
【解决方案2】:

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

  1. 第一个(5 &gt; 8) ? 4 : null被求值,第二个操作数是int,第三个是null,如果我们在表中查找,这个表达式的结果类型是Integer(换句话说:因为其中一个操作数是null,所以这被视为引用条件表达式
  2. 然后我们要评估(5&gt;7) ? 3 : &lt;previous result&gt;,这意味着在上面链接的表格中,我们需要查找第二个操作数int和第三个操作数Integer的结果类型:它是int。这意味着 &lt;previous result&gt; 需要被取消装箱,并以 NPE 失败。

那么为什么第一个案例有效呢?

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

但是,这仅发生在 null 文字上,以下代码仍会抛出 NPE,因为操作数类型 intInteger 会导致 数字条件表达式

Integer i = null;
Integer x = (5>7) ? 3 : i;

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

  1. 如果两个操作数的编译类型均为Integer,则结果为Integer
  2. 如果一个操作数的类型为int,另一个为Integer,则结果为int
  3. 如果一个操作数是null type(其中唯一的有效值是null 引用),则结果是Integer

【讨论】:

  • 不错的总结。知道为什么 JLS 中的这些表指定 ? int : Integer? Integer : int 映射到 int 而不是 Integer?
  • @AndyThomas 只能猜测,但我认为大多数情况下,您无论如何都会将结果分配给 int 变量,并为结果只是立即丢弃它被认为太昂贵了。但这真的只是一个猜测,可能还有其他更有效的原因。
  • 好的,为什么 是一个 int 但在第一个与 形式相同的示例中,答案是 Integer?
  • @DavidStigant &lt;previous result&gt; 在这两种情况下都是 Integer,这就是导致问题的原因。
  • 但是只有第二种情况有问题,第一种没有。或者,只有第二种情况会导致 NPE,而不是第一种。
【解决方案3】:
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,而是 Integer。

【讨论】:

    猜你喜欢
    • 2015-07-13
    • 2021-03-17
    • 2014-01-21
    • 2021-11-27
    • 2021-02-22
    • 1970-01-01
    • 1970-01-01
    • 2011-12-21
    相关资源
    最近更新 更多