繁体   English   中英

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

[英]Difference in behaviour of the ternary operator on JDK8 and 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);
    }
}

在 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

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

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

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

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

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

    • […]

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

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

    • […]

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

[…]

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

  • […]

  • 如果第二个和第三个操作数之一是原始类型T ,另一个的类型是对T应用装箱转换(第 5.1.7 节)的结果,则条件表达式的类型为T

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

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

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

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

对于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);
        }
    }
}

该程序在 Java 8 上的输出是:

a == null
java.lang.NullPointerException

换句话说,尽管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"));
    }
}

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

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

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

根据 JLS 8 和 JLS 10,如果第二个表达式 ( 1.0 ) 是double类型,而第三个 ( new HashMap<String, Double>().get("1") ) 是Double类型,则结果条件表达式的类型为double 在JVM中的Java 8似乎是足够聪明,知道,因为你返回一个Double ,没有理由先拆箱的结果HashMap#get一个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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM