JDK8和JDK10上三元运算符的行为差异

Ser*_*rCe 57 java unboxing javac java-8 java-10

请考虑以下代码

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

在JDK8上运行时,此代码会打印,null而在JDK10上会生成此代码NullPointerException

Exception in thread "main" java.lang.NullPointerException
    at JDK10Test.main(JDK10Test.java:5)
Run Code Online (Sandbox Code Playgroud)

除了JDK10编译器生成的两个与自动装箱相关的附加指令外,编译器生成的字节码几乎完全相同,并且似乎负责NPE.

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

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

JDK8:  java version "1.8.0_172"
JDK10: java version "10.0.1" 2018-04-17
Run Code Online (Sandbox Code Playgroud)

Rad*_*def 47

我相信这是一个似乎已经解决的错误.NullPointerException根据JLS,投掷似乎是正确的行为.

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

该行为应取决于get方法的返回类型,如下面§15.26的摘录所述:

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

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

    • [...]

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

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

    • [...]

  • 否则,条件表达式是引用条件表达式.

[...]

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

  • [...]

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

换句话说,如果两个表达式都可以转换为数字类型,并且一个是基本的而另一个是盒装的,则三元条件的结果类型是基本类型.

(表15.25-C也方便地向我们表明三元表达的类型boolean ? double : Double确实是double,再次意味着拆箱和投掷是正确的.)

如果方法的返回类型get不可转换为数字类型,则三元条件将被视为"引用条件表达式",并且不会发生拆箱.

另外,我认为注释"对于泛型方法,这是在实例化方法的类型参数之前的类型"不应该适用于我们的情况.Map.get不声明类型变量,因此它不是JLS定义的通用方法.但是,这个注释在Java 9 添加的(唯一的变化,请参阅JLS8),因此它可能与我们今天看到的行为有关.

对于a HashMap<String, Double>,返回类型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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Java 8上该程序的输出是:

a == null
java.lang.NullPointerException
Run Code Online (Sandbox Code Playgroud)

换句话说,尽管e.nullAsNumber()e.nullAsDouble()具有相同的实际返回类型,只有e.nullAsDouble()被认为是一个"数字表达".方法之间的唯一区别是类型变量绑定.

可能会有更多的调查,但我想发布我的发现.我尝试了很多东西,发现只有当表达式是返回类型中带有类型变量的方法时,错误(即没有取消装箱/ NPE)才会发生.


有趣的是,我发现以下程序也引入了Java 8:

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"));
    }
}
Run Code Online (Sandbox Code Playgroud)

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

(最初我想使用重载来证明编译器给三元表达式提供的实际类型,但鉴于上述差异,它看起来不太可能.有可能还有另一种我没有想到的方法,虽然.)


Jac*_* G. 12

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

根据JLS 8和JLS 10,如果第二个表达式(1.0)是类型double而第三个(new HashMap<String, Double>().get("1"))是类型Double,那么条件表达式的结果是类型double.Java 8中的JVM似乎足够聪明,因为你正在返回a Double,所以没有理由首先将结果解包HashMap#get到a double然后将其重新打包回Double(因为你指定Double).

为了证明这一点,更改Doubledouble在你的榜样,并且NullPointerException被抛出(在JDK 8); 这是因为拆箱现在正在发生,null.doubleValue()显然会抛出一个NullPointerException.

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

这似乎改变了10,但我不能告诉你原因.

  • @AbhijitSarkar根据JLS 8和JLS 10,条件表达式的返回类型在这种情况下是"double",但它被装箱为"Double",因为OP指定了它. (6认同)