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

标签 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);
    }
}

在 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

最佳答案

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

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

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

  • If both the second and the third operand expressions are numeric expressions, the conditional expression is a numeric conditional expression.

    For the purpose of classifying a conditional, the following expressions are numeric expressions:

    • […]

    • A method invocation expression (§15.12) for which the chosen most specific method (§15.12.2.5) has a return type that is convertible to a numeric type.

      Note that, for a generic method, this is the type before instantiating the method's type arguments.

    • […]

  • Otherwise, the conditional expression is a reference conditional expression.

[…]

The type of a numeric conditional expression is determined as follows:

  • […]

  • If one of the second and third operands is of primitive type T, and the type of the other is the result of applying boxing conversion (§5.1.7) to T, then the type of the conditional expression is T.

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

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

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

另外,我认为注释“对于泛型方法,这是实例化方法的类型参数之前的类型” 不应该适用于我们的案例。 Map.get不声明类型变量,so it's not a generic method by the JLS' definition .但是,此注释是在 Java 9 中添加的(唯一的更改,see 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);
        }
    }
}

The output of that program on Java 8是:

a == null
java.lang.NullPointerException

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

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


有趣的是,我发现 the following program also throws在 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"));
    }
}

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

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

关于java - 三元运算符在 JDK8 和 JDK10 上的行为差异,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50769880/

相关文章:

Java8 相当于 JodaTime DateTimeFormat.shortDate()

javac - 如何增加 Borland JBuilder 2005/2006 中 javac 进程的最大堆大小

java - 优化期间会使用 Java 内联方法吗?

java - 为什么java编译器不提示这个:

java - JTable 将单元格颜色设置为特定值

java - BigInteger.longValue() 是否保证保留 2^63 <= x <= 2^64-1 的无符号二进制?

java - 在 Java 8 中对两个 ArrayList 进行乘法和求和

java - 为什么在 Java 中使用 lambda 表达式?

java - UTF8编码问题?

Java从字符串中获取整数值