三元运算符在 JDK8 和 JDK10 上的行为差异 [英] Difference in behaviour of the ternary operator on JDK8 and JDK10

查看:36
本文介绍了三元运算符在 JDK8 和 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

When running on JDK8, this code prints null whereas on JDK10 this code results in NullPointerException

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

编译器产生的字节码几乎相同,除了 JDK10 编译器产生的两条与自动装箱相关的额外指令外,似乎是 NPE 的责任.

The bytecode produced by the compilers is almost identical apart from two additional instructions produced by the JDK10 compiler which are related to autoboxing and seem to be responsible for the NPE.

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

此行为是 JDK10 中的错误还是有意更改以使行为更严格?

Is this behaviour a bug in JDK10 or an intentional change to make the behaviour stricter?

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

推荐答案

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

I believe this was a bug which seems to have been fixed. Throwing a NullPointerException seems to be the correct behavior, according to the JLS.

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

I think that what is going on here is that for some reason in version 8, the compiler considered the bounds of the type variable mentioned by the method's return type rather than the actual type arguments. In other words, it thinks ...get("1") returns Object. This could be because it's considering the method's erasure, or some other reason.

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

The behavior should hinge upon the return type of the get method, as specified by the below excerpts from §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:

  • […]

  • […]

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

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

[…]

否则,条件表达式为引用条件表达式.

Otherwise, the conditional expression is a reference conditional expression.

[…]

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

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

  • […]

  • […]

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

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.

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

In other words, if both expressions are convertible to a numeric type, and one is primitive and the other is boxed, then the result type of the ternary conditional is the primitive type.

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

(Table 15.25-C also conveniently shows us that the type of a ternary expression boolean ? double : Double would indeed be double, again meaning unboxing and throwing is correct.)

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

If the return type of the get method wasn't convertible to a numeric type, then the ternary conditional would be considered a "reference conditional expression" and unboxing wouldn't occur.

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

Also, I think the note "for a generic method, this is the type before instantiating the method's type arguments" shouldn't apply to our case. Map.get doesn't declare type variables, so it's not a generic method by the JLS' definition. However, this note was added in Java 9 (being the only change, see JLS8), so it's possible that it has something to do with the behavior we're seeing today.

对于HashMapget的返回类型应该Double.

这里有一个 MCVE 支持我的理论,即编译器正在考虑类型变量边界而不是实际类型参数:

Here's an MCVE supporting my theory that the compiler is considering the type variable bounds rather than the actual type arguments:

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() 被视为数字表达式".方法之间的唯一区别是类型变量绑定.

In other words, despite e.nullAsNumber() and e.nullAsDouble() having the same actual return type, only e.nullAsDouble() is considered as a "numeric expression". The only difference between the methods is the type variable bound.

可能还有更多调查要做,但我想发布我的发现.我尝试了很多事情,发现错误(即没有拆箱/NPE)似乎只在表达式是返回类型中具有类型变量的方法时才会发生.

There's probably more investigation that could be done, but I wanted to post my findings. I tried quite a few things and found that the bug (i.e. no unboxing/NPE) seems to only happen when the expression is a method with a type variable in the return type.

有趣的是,我发现以下程序也在 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"));
    }
}

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

That shows that the compiler's behavior is actually different, depending on whether the ternary expression is assigned to a local variable or a method parameter.

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

(Originally I wanted to use overloads to prove the actual type that the compiler is giving to the ternary expression, but it doesn't look like that's possible given the above difference. It's possible there's still another way that I haven't thought of, though.)

这篇关于三元运算符在 JDK8 和 JDK10 上的行为差异的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆