问题描述
考虑以下代码
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:
出于分类条件的目的,以下表达式是数字表达式:
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.
对于HashMap<String, Double>
,get
的返回类型应为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);
}
}
}
The output of that program on Java 8 is:
a == null
java.lang.NullPointerException
换句话说,尽管e.nullAsNumber()
和e.nullAsDouble()
具有相同的实际返回类型,但仅将e.nullAsDouble()
视为数值表达式".方法之间的唯一区别是类型变量绑定.
可能还有更多的调查可以做,但是我想发表我的发现.我尝试了很多事情,发现该错误(即没有取消装箱/NPE)似乎仅在表达式是带有返回类型的类型变量的方法时发生.
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上三元运算符的行为差异的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!