Java JDK8和JDK10上三元算子行为的差异

Java JDK8和JDK10上三元算子行为的差异,java,java-8,javac,unboxing,java-10,Java,Java 8,Javac,Unboxing,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); } } 编译器生成的字节码几乎完全相同,除了JDK10编译器生成的两条附加指令外,这两条指令与自动装箱相关,并且似乎

考虑以下代码

public class JDK10Test {
    public static void main(String[] args) {
        Double d = false ? 1.0 : new HashMap<String, Double>().get("1");
        System.out.println(d);
    }
}
编译器生成的字节码几乎完全相同,除了JDK10编译器生成的两条附加指令外,这两条指令与自动装箱相关,并且似乎负责NPE

15: invokevirtual #7                  // Method java/lang/Double.doubleValue:()D
18: invokestatic  #8                  // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
这种行为是JDK10中的一个bug,还是为了使行为更加严格而有意进行的更改

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

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

根据JLS 8和JLS 10,如果第二个表达式(
1.0
)的类型为
double
,第三个表达式(
new HashMap().get(“1”)
)的类型为
double
,则条件表达式的结果的类型为
double
。Java8中的JVM似乎足够聪明,可以知道,因为您返回的是一个
Double
,所以没有理由首先将
HashMap的结果取消装箱#获取
到一个
Double
,然后将其装箱回
Double
(因为您指定了
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
double d=false?1.0:新的HashMap().get(“1”);
系统输出打印Ln(d);//抛出NullPointerException

这似乎在10年内有所改变,但我无法告诉您原因。

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

我认为这里发生的事情是,出于某种原因,在版本8中,编译器考虑了方法的返回类型所提到的类型变量的边界,而不是实际的类型参数。换句话说,它认为,
…get(“1”)
返回
对象。这可能是因为它考虑了方法的擦除,或者其他一些原因

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

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

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

    • [……]

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

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

    • [……]

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

[……]

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

  • [……]

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

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

(表15.25-C还方便地向我们展示了三元表达式的类型
布尔?double:double
实际上是
double
,这同样意味着拆箱和抛出是正确的。)

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

此外,我认为“对于泛型方法,这是实例化该方法的类型参数之前的类型”不应适用于我们的案例
Map.get
不声明类型变量。然而,这个注释是在Java9中添加的(这是唯一的更改),所以它可能与我们今天看到的行为有关

对于
HashMap
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);
        }
    }
}
换句话说,尽管
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"));
    }
}
import java.util.*;
课例{
静态无效接受(双d){}
公共静态void main(字符串[]args){
接受(false?1.0:newhashmap().get(“1”));
}
}
这表明编译器的行为实际上是不同的,这取决于三元表达式是分配给局部变量还是方法参数


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

我对在intelliJ Java(TM)中运行的这段代码没有问题SE运行时环境18.3(构建10.0.1+10)2018-04-17@Radiodef对我来说,
false?1.0:(双精度)null
抛出一个
NPE
a == null
java.lang.NullPointerException
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"));
    }
}