重大线上事故!三元表达式引发的空指针问题

属实刺激,刚入职不久就遇到这种史诗级的线上 Bug,首页直接崩溃,陈年老代码爆雷,不管落到最后的底层原因是什么,我感觉主要还是上下游的链路太过复杂,治理难度比较大,牵一发而动全身 。
知识回顾三目运算符大家都很熟悉了:
【重大线上事故!三元表达式引发的空指针问题】<表达式1> ? <表达式2> : <表达式3>我习惯称为三元表达式,需要注意的就是:**一个三元表达式从不会既计算 <表达式 2>,又计算 <表达式 3>** 。条件运算符是右结合的,也就是说,从右向左分组计算 。例如,a ? b : c ? d : e 将按 a ? b : (c ? d : e) 执行 。
再来回顾下自动拆箱和装箱机制,JAVA 通过这种机制使得包装类和基本数据类型之间的转换更加方便:

  • 装箱:将基本数据类型转换成包装类(每个包装类的构造方法都可以接收各自数据类型的变量)
  • 拆箱:从包装类之中取出被包装的基本类型数据(使用包装类的 xxxValue 方法)
下面以 Integer 为例,我们来看看 Java 内置的包装类是如何进行拆装箱的:
Integer obj = new Integer(10);// 装箱int temp = obj.intValue();// 拆箱这种形式的代码是 JDK 1.5 以前的,JDK 1.5 之后,Java 设计者为了方便开发提供了自动装箱(Autoboxing)与自动拆箱的机制,并且可以直接利用包装类的对象进行数学计算 。
还是以 Integer 为例我们来看看自动拆装箱的过程:
Integer obj = 10;// 自动装箱. 基本数据类型 int -> 包装类 Integerint temp = obj;// 自动拆箱. Integer -> intobj ++; // 直接利用包装类的对象进行数学计算System.out.println(temp * obj);基本数据类型到包装类的转换,不需要像上面一样使用构造函数,直接 = 就完事儿;同样的,包装类到基本数据类型的转换,也不需要我们手动调用包装类的 xxxValue 方法了,直接 = 就能完成拆箱 。这也是将它们称之为自动的原因 。
重大线上事故!三元表达式引发的空指针问题

文章插图
图片
我们来看看这段代码反编译后的文件,底层到底是什么原理:
Integer obj = Integer.valueOf(10);int temp = obj.intValue();可以看见,自动装箱的底层原理其实就是调用了包装类的 valueOf 方法,而自动拆箱的底层同样还是调用了包装类的 intValue() 方法 。
重大线上事故!三元表达式引发的空指针问题

文章插图
图片
问题重现实际的代码业务逻辑比较复杂,这里我们举一个相对简单的一点的例子先来重现下这个问题:
// 设置成true,保证条件表达式的表达式二一定可以执行boolean flag = true;//定义一个包装类对象类型的Boolean变量,值为null Boolean nullBoolean = null;// 定义一个基本数据类型的boolean变量boolean simpleBoolean = false; //使用三目运算符并给 x 变量赋值boolean x = flag ? nullBoolean : simpleBoolean;以上代码,在运行过程中,会抛出 NPE:
Exception in thread "mAIn" java.lang.NullPointerException而且,这个和你使用的 JDK 版本是无关的,我在 JDK 6、JDK 8 和 JDK 14 上做了测试,均会抛出 NPE 。
尝试对以上代码进行反编译,使用 jad 工具进行反编译后,得到以下代码:
boolean flag = true;boolean simpleBoolean = false;Boolean nullBoolean = null;boolean x = flag ? nullBoolean.booleanValue() : simpleBoolean;可以看到,反编译后的代码的最后一行,编译器帮我们做了一次自动拆箱(nullBoolean 是包装类,而 x 是基本类型),而 nullBoolean 是 null,这就出现了 null.booleanValue,从而抛出 NPE 。
那么,为什么编译器会进行自动拆箱呢?什么情况下需要进行自动拆箱呢?
原理分析关于为什么编辑器会在代码编译阶段对于三目运算符中的表达式进行自动拆箱,其实在《The Java Language Specification》(后文简称 JLS,是Java 语言规范,是一切 Java 编程的基础参照文档)的第 15.25 章节中是有相关介绍的 。我们直接看 Java SE 1.7 JLS 中关于这部分的描述(因为 1.7 的表述更加简洁一些),原文地址 -> https://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.25:
重大线上事故!三元表达式引发的空指针问题

文章插图
图片
看我框出来的两句话:
  1. If the second and third operands have the same type (which may be the null type),then that is the type of the conditional expression. 当第二位和第三位操作数的类型相同时,则三目运算符表达式的结果和这两位操作数的类型相同


    推荐阅读