三元运算符在 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 10 似乎没有指定对条件运算符的任何更改,但我有一个理论。
根据 JLS 8 和 JLS 10,如果第二个表达式 (1.0
) 的类型为 double
,第三个表达式 (new HashMap<String, Double>().get("1")
) 的类型为 Double
, 则条件表达式的结果为 double
类型。 Java 8 中的 JVM 似乎足够聪明,知道这一点,因为您返回的是 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 中更改的,但我不能告诉你为什么。
我认为这是一个似乎已修复的错误。根据 JLS,抛出 NullPointerException
似乎是正确的行为。
我认为这里发生的事情是由于某些原因在版本 8 中,编译器考虑了方法的 return 类型所提及的类型变量的边界,而不是实际的类型参数。换句话说,它认为 ...get("1")
returns Object
。这可能是因为它正在考虑方法的擦除,或者其他一些原因。
该行为应取决于 get
方法的 return 类型,如以下摘录自 §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
.
换句话说,如果两个表达式都可以转换为数值类型,并且一个是原始类型,另一个是装箱类型,那么三元条件的结果类型就是原始类型。
(Table 15.25-C 还方便地向我们展示了三元表达式 boolean ? double : Double
的类型确实是 double
,这再次意味着拆箱和抛出是正确的。)
如果 get
方法的 return 类型不能转换为数字类型,则三元条件将被视为 "reference conditional expression" 并且不会发生拆箱。
此外,我认为注释 "for a generic method, this is the type before instantiating the method's type arguments" 不应该适用于我们的案例。 Map.get
没有声明类型变量,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),所以它可能与我们今天看到的行为有关。
对于HashMap<String, Double>
,get
的return类型应该是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()
具有相同的实际 return 类型,但只有 e.nullAsDouble()
被视为 "numeric expression"。这些方法之间的唯一区别是类型变量绑定。
可能还有更多调查可以完成,但我想 post 我的发现。我尝试了很多东西,发现错误(即没有 unboxing/NPE)似乎只发生在表达式是 return 类型的类型变量的方法时。
有趣的是,我发现 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"));
}
}
这表明编译器的行为实际上是不同的,这取决于三元表达式是分配给局部变量还是方法参数。
(最初我想使用重载来证明编译器赋予三元表达式的实际类型,但考虑到上述差异,这看起来不太可能。可能还有另一种方法我没有不过没想到。)
考虑以下代码
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 10 似乎没有指定对条件运算符的任何更改,但我有一个理论。
根据 JLS 8 和 JLS 10,如果第二个表达式 (1.0
) 的类型为 double
,第三个表达式 (new HashMap<String, Double>().get("1")
) 的类型为 Double
, 则条件表达式的结果为 double
类型。 Java 8 中的 JVM 似乎足够聪明,知道这一点,因为您返回的是 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 中更改的,但我不能告诉你为什么。
我认为这是一个似乎已修复的错误。根据 JLS,抛出 NullPointerException
似乎是正确的行为。
我认为这里发生的事情是由于某些原因在版本 8 中,编译器考虑了方法的 return 类型所提及的类型变量的边界,而不是实际的类型参数。换句话说,它认为 ...get("1")
returns Object
。这可能是因为它正在考虑方法的擦除,或者其他一些原因。
该行为应取决于 get
方法的 return 类型,如以下摘录自 §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) toT
, then the type of the conditional expression isT
.
换句话说,如果两个表达式都可以转换为数值类型,并且一个是原始类型,另一个是装箱类型,那么三元条件的结果类型就是原始类型。
(Table 15.25-C 还方便地向我们展示了三元表达式 boolean ? double : Double
的类型确实是 double
,这再次意味着拆箱和抛出是正确的。)
如果 get
方法的 return 类型不能转换为数字类型,则三元条件将被视为 "reference conditional expression" 并且不会发生拆箱。
此外,我认为注释 "for a generic method, this is the type before instantiating the method's type arguments" 不应该适用于我们的案例。 Map.get
没有声明类型变量,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),所以它可能与我们今天看到的行为有关。
对于HashMap<String, Double>
,get
的return类型应该是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()
具有相同的实际 return 类型,但只有 e.nullAsDouble()
被视为 "numeric expression"。这些方法之间的唯一区别是类型变量绑定。
可能还有更多调查可以完成,但我想 post 我的发现。我尝试了很多东西,发现错误(即没有 unboxing/NPE)似乎只发生在表达式是 return 类型的类型变量的方法时。
有趣的是,我发现 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"));
}
}
这表明编译器的行为实际上是不同的,这取决于三元表达式是分配给局部变量还是方法参数。
(最初我想使用重载来证明编译器赋予三元表达式的实际类型,但考虑到上述差异,这看起来不太可能。可能还有另一种方法我没有不过没想到。)