在三目运算符中,表达式 1 和 2 在涉及算术计算或数据类型转换时,会触发自动拆箱。当其中的操作数为 null 值时,会导致 NPE 。
一、基础知识
三目运算符
三目运算符是 Java 语言中的重要组成部分,它也是唯一有 3 个操作数的运算符。形式为:
<表达式1> ? <表达式2> : <表达式3>
以上,通过 ?、: 组合的形式得到一个条件表达式。其中 ? 运算符的含义是:先求表达式 1 的值,如果为真,则执行并返回表达式 2 的结果;如果表达式 1 的值为假,则执行并返回表达式 3 的结果。
自动装箱与自动拆箱
Java自动拆箱空指针异常
二、问题重现
如下代码在执行是会抛出空指针异常
Double a = null;
Integer b = null;
Double c = Objects.nonNull(b) ? b : a;
异常:
三、原理
以上代码反编译出字节码如下:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: aconst_null
1: astore_1
2: aconst_null
3: astore_2
4: aload_2
5: invokestatic #2 // Method java/util/Objects.nonNull:(Ljava/lang/Object;)Z
8: ifeq 19
11: aload_2
12: invokevirtual #3 // Method java/lang/Integer.intValue:()I
15: i2d
16: goto 23
19: aload_1
20: invokevirtual #4 // Method java/lang/Double.doubleValue:()D
23: invokestatic #5 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
26: astore_3
27: return
#12行可以看出b Integer类型变量开箱为int值,#20行可以看出a Double变量开箱为double值,#23行可以看出将根据条件,得出的double/int值装箱为Double类型的对象。翻译成java代码如下:
Double c = Double.valueOf(Objects.nonNull(b) ? b.intValue() : a.doubleValue());
b,a都是null值,因此调用函数时报出npe异常。
分析之后我们可以得出这样的结论:三目运算符和自动拆箱导致了空指针异常。
那么,为什么编译器会进行自动拆箱呢?什么情况下需要进行自动拆箱呢?这其实是三目运算符的语法规范。参见jls-15.25,摘要如下:
以去翻阅一下。
其实简单总结下,就是:
1)如果第二个和第三个操作数具有相同的类型(可能是null类型),那么这就是条件表达式的类型。
2)如果第二个和第三个操作数中的一个是基本类型T,而另一个操作数的类型是对T应用装箱转换(§5.1.7)的结果,则条件表达式的类型为T。
3)如果第二个和第三个操作数中的一个是空类型,而另一个操作数的类型是引用类型,则条件表达式的类型就是该引用类型。
否则,如果第二个和第三个操作数的类型可转换为数值类型(§5.1.8),则有以下几种情况:
1)如果一个操作数是byte或Byte类型,另一个是short或Short类型,则条件表达式的类型为short。
2)如果其中一个操作数是T类型,其中T是byte、short或char,而另一个操作数是int类型的常量表达式(§15.28),其值可以用T类型表示,则条件表达式的类型为T。
3)如果其中一个操作数是T类型,其中T是字节、短或字符,而另一个操作数是int类型的常量表达式(§15.28),其值可以用U类型表示,U是对T应用开箱转换的结果,那么条件表达式的类型是U。
否则,二进制数值提升(§5.6.2)应用于操作数类型,条件表达式的类型是第二个和第三个操作数的提升类型。注意,二进制数字提升执行值集转换(§5.1.13),并可能执行开箱转换(§5.1.8)。
值集转换规则:
当操作符对一对操作数应用二进制数字提升,每个操作数必须表示一个可转换为数字类型的值时,应用以下规则,顺序:
1)如果任何操作数是引用类型,它将进行开箱转换(§5.1.8)。
2)扩展基元转换(§5.1.2)应用于按以下规则指定的任意一个或两个操作数转换:
- 如果其中一个操作数为double类型,则另一个操作数转换为double类型。
- 否则,如果其中一个操作数为float类型,则另一个操作数转换为float类型。
- 否则,如果其中一个操作数为long类型,则另一个操作数转换为long类型。
- 否则,两个操作数都转换为int类型。
我自己的话总结一下,npe其实是值集转换引起的,当以下操作符不能自然转换时(比如一个值为byte,一个值为short,可以),会产生值集转换,而根据值集转换规则1),任何引用类型都要开箱,那么空值开箱就会导致NPE。
最根的解决方法就是阿里巴巴新出的规范: