运行以下 Java 代码:
boolean b = false;
Double d1 = 0d;
Double d2 = null;
Double d = b ? d1.doubleValue() : d2;
为什么会出现 NullPointerException?
运行以下 Java 代码:
boolean b = false;
Double d1 = 0d;
Double d2 = null;
Double d = b ? d1.doubleValue() : d2;
为什么会出现 NullPointerException?
条件表达式的返回类型b ? d1.doubleValue : d2
是double
。条件表达式必须有一个返回类型。遵循二进制数字提升的规则,d2
自动拆箱到 a double
,这会导致NullPointerException
when d2 == null
。
从语言规范第 15.25 节:
否则,如果第二个和第三个操作数具有可转换(第 5.1.8 节)为数字类型的类型,则有几种情况: ...
否则,二进制数值提升(第 5.6.2 节)将应用于操作数类型,条件表达式的类型是第二个和第三个操作数的提升类型。请注意,二进制数字提升执行拆箱转换(第 5.1.8 节)和值集转换(第 5.1.13 节)。
因为周围的两个表达式:
必须返回相同的类型。这意味着 Java 会尝试将表达式转换d2
为double
. 这意味着字节码调用doubleValue()
- d2
> NPE。
您通常应该避免混合类型计算;?:
将其与条件/三元复合只会使情况变得更糟。
这是来自Java Puzzlers的引述,Puzzle 8: Dos Equis:
混合类型的计算可能会令人困惑。这在条件表达式中表现得最为明显。[...]
确定条件表达式结果类型的规则太长太复杂,无法完整重现,但这里有三个关键点。
如果第二个和第三个操作数的类型相同,那就是条件表达式的类型。换句话说,您可以通过避开混合类型的计算来避免整个混乱。
如果其中一个操作数是T类型,其中T是
byte
、short
或,而另一个操作数是其值可在类型Tchar
中表示的类型的常量表达式,则条件表达式的类型是T。int
否则,二进制数值提升应用于操作数类型,条件表达式的类型是第二个和第三个操作数的提升类型。
此处应用了第 3 点,它导致了拆箱。当你 unbox 时null
,自然NullPointerException
会抛出 a 。
这是混合类型计算的另一个示例,?:
这可能令人惊讶:
Number n = true ? Integer.valueOf(1) : Double.valueOf(2);
System.out.println(n); // "1.0"
System.out.println(n instanceof Integer); // "false"
System.out.println(n instanceof Double); // "true"
混合类型计算是至少 3 个Java Puzzlers的主题。
最后,这是Java Puzzlers的规定:
4.1。混合类型的计算令人困惑
处方:避免混合类型的计算。
将
?:
运算符与数字操作数一起使用时,请对第二个和第三个操作数使用相同的数字类型。
这是来自Effective Java 2nd Edition, Item 49: Prefer primitive types to boxed primitives的引述:
总之,只要您有选择,就优先使用原语而不是盒装原语。原始类型更简单、更快。如果您必须使用盒装图元,请小心!自动装箱减少了使用装箱原语的冗长,但不会降低危险。当您的程序将两个装箱原语与
==
运算符进行比较时,它会进行身份比较,这几乎肯定不是您想要的。当您的程序进行涉及装箱和拆箱原语的混合类型计算时,它会拆箱,而当您的程序拆箱时,它可以抛出NullPointerException
. 最后,当您的程序将原始值装箱时,可能会导致成本高昂且不必要的对象创建。
有些地方你别无选择,只能使用盒装原语,例如泛型,但否则你应该认真考虑使用盒装原语的决定是否合理。
为两种情况返回相同的类型,如下所示,您将得到结果。
boolean b = false;
Double d1 = 0d;
Double d2 = null;
Double d = b ? d1 : (Double)d2;
System.out.println(d);