【问题标题】:Difference in behaviour of the ternary operator on JDK8 and JDK10三元运算符在 JDK8 和 JDK10 上的行为差异
【发布时间】:2018-11-19 01:26:36
【问题描述】:

考虑下面的代码

public class JDK10Test {
    public static void main(String[] args) {
        Double d = false ? 1.0 : new HashMap<String, Double>().get("1");
        System.out.println(d);
    }
}

在 JDK8 上运行时,此代码输出 null,而在 JDK10 上,此代码输出 NullPointerException

Exception in thread "main" java.lang.NullPointerException
    at JDK10Test.main(JDK10Test.java:5)

编译器生成的字节码几乎是相同的,除了 JDK10 编译器生成的两个额外指令,它们与自动装箱相关并且似乎负责 NPE。

15: invokevirtual #7                  // Method java/lang/Double.doubleValue:()D
18: invokestatic  #8                  // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;

这种行为是 JDK10 中的错误还是故意更改以使行为更严格?

JDK8:  java version "1.8.0_172"
JDK10: java version "10.0.1" 2018-04-17

【问题讨论】:

  • 这段代码在 intelliJ Java(TM) SE Runtime Environment 18.3 (build 10.0.1+10) 2018-04-17 中运行没有问题
  • @Radiodef 对我来说,false ? 1.0 : (Double) null 为两个 JDK(JDK 1.8.0_144 和 JDK 10.0.1)抛出一个 NPE,而 new HashMap&lt;...&gt;...-version 只为 JDK 10.0 抛出。 1.
  • 我认为投掷是正确的行为,所以我猜这是/曾经是一个错误。见§15.25 under Table 15.25-D:结果类型应该是double,所以应该是unboxed。
  • @Radiodef 在 Java 8 规范中也是如此。

标签: java java-8 javac unboxing java-10


【解决方案1】:

我相信这是一个似乎已修复的错误。根据 JLS,投掷 NullPointerException 似乎是正确的行为。

我认为这里发生的情况是,出于某种原因,在版本 8 中,编译器考虑了方法返回类型提到的类型变量的边界,而不是实际的类型参数。换句话说,它认为...get("1") 返回Object。这可能是因为它正在考虑删除该方法,或其他原因。

行为应取决于get 方法的返回类型,正如以下§15.26 的摘录所指定的那样:

  • 如果第二个和第三个操作数表达式都是numeric表达式,则条件表达式是一个数字条件表达式。

    为了对条件进行分类,以下表达式为数值表达式:

    • […]

    • 方法调用表达式(第 15.12 节),为其选择的最具体的方法(第 15.12.2.5 节)具有可转换为数字类型的返回类型。

      请注意,对于泛型方法,这是实例化方法的类型参数之前的类型。

    • […]

  • 否则条件表达式为引用条件表达式。

[…]

数值条件表达式的类型确定如下:

  • […]

  • 如果第二个和第三个操作数之一是基本类型 T,而另一个的类型是对 T 应用装箱转换 (§5.1.7) 的结果,那么条件表达式为T

换句话说,如果两个表达式都可以转换为数字类型,并且一个是原始的,另一个是装箱的,那么三元条件的结果类型就是原始类型。

(表 15.25-C 还方便地向我们展示了三元表达式 boolean ? double : Double 的类型确实是 double,这再次意味着拆箱和投掷是正确的。)

如果get 方法的返回类型不能转换为数值类型,则三元条件将被视为“引用条件表达式”,不会发生拆箱。

另外,我认为注释“对于泛型方法,这是实例化方法的类型参数之前的类型” 不应该适用于我们的案例。 Map.get 没有声明类型变量,so it's not a generic method by the JLS' definition。但是,此注释在 Java 9 中添加的(这是唯一的更改,see JLS8),因此它可能与我们今天看到的行为有关。

对于HashMap&lt;String, Double&gt;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);
        }
    }
}

The output of that program on Java 8 是:

a == null
java.lang.NullPointerException

换句话说,尽管e.nullAsNumber()e.nullAsDouble() 具有相同的实际返回类型,但只有e.nullAsDouble() 被视为“数值表达式”。方法之间的唯一区别是类型变量绑定。

可能需要进行更多调查,但我想发布我的调查结果。我尝试了很多东西,发现错误(即没有拆箱/NPE)似乎只发生在表达式是返回类型中具有类型变量的方法时。


有趣的是,我在 Java 8 中发现了 the following program also throws

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"));
    }
}

这表明编译器的行为实际上是不同的,这取决于三元表达式是分配给局部变量还是方法参数。

(最初我想使用重载来证明编译器赋予三元表达式的实际类型,但鉴于上述差异,这看起来不太可能。可能还有另一种我没有的方法不过也有想过。)

【讨论】:

    【解决方案2】:

    JLS 10 似乎没有对条件运算符指定任何更改,但我有一个理论。

    根据 JLS 8 和 JLS 10,如果第二个表达式 (1.0) 的类型为 double,而第三个表达式 (new HashMap&lt;String, Double&gt;().get("1")) 的类型为 Double,则条件表达式的结果为输入double。 Java 8 中的 JVM 似乎足够聪明地知道,因为您返回的是 Double,所以没有理由先将 HashMap#get 的结果拆箱到 double,然后再将其装箱回 @987654329 @(因为你指定了Double)。

    为了证明这一点,在您的示例中将Double 更改为double,并抛出NullPointerException(在JDK 8 中);这是因为现在正在进行拆箱,null.doubleValue() 显然会抛出 NullPointerException

    double d = false ? 1.0 : new HashMap<String, Double>().get("1");
    System.out.println(d); // Throws a NullPointerException
    

    这似乎是在 10 中改变的,但我不能告诉你为什么。

    【讨论】:

    • > "...条件表达式的结果是double 类型" 类型应该是Double,包装类型,而不是原始类型。换句话说,1.0 被装箱,null 没有被拆箱。
    • @AbhijitSarkar 根据 JLS 8 和 JLS 10,在这种情况下条件表达式的返回类型是 double,但由于 OP 指定了它,它被装箱为 Double
    猜你喜欢
    • 1970-01-01
    • 2018-09-18
    • 2011-10-08
    • 2015-12-05
    • 2012-08-08
    • 1970-01-01
    • 2020-05-10
    • 2017-03-05
    相关资源
    最近更新 更多