最近,小黑哥在一个业务改造中,使用三目运算符重构了业务代码,没想到测试的时候居然发生 NPE 的问题。html
重构代码很是简单,代码以下:java
// 方法返回参数类型为 Integer
// private Integer code;
SimpleObj simpleObj = new SimpleObj();
// 其余业务逻辑
if (simpleObj == null) {
return -1;
} else {
return simpleObj.getCode();
}
复制代码
这段 if 判断,小黑哥看到的时候,感受非常繁琐,因而使用条件表达式重构了一把,代码以下:程序员
// 方法返回参数类型为 Integer
SimpleObj simpleObj = new SimpleObj();
// 其余业务逻辑
return simpleObj == null ? -1 : simpleObj.getCode();
复制代码
测试的时候,第四行代码抛出了空指针,这里代码很简单,显然只有 simpleObj#getCode
才有可能发生 NPE 问题。express
可是我明明为 simpleObj
作过判空判断,simpleObj
对象确定不是 null,那么只有 simpleObj#getCode
返回为 null。可是个人代码并无对这个方法返回值作任何操做,为什么会触发 NPE?bash
难道是又是自动拆箱致使的 NPE 问题?微信
在解答这个问题以前,咱们首先复习一下条件表达式。oracle
点赞再看,养成习惯。微信搜索『程序通事』,关注查看最新文章~app
三目运算符,官方英文名称:Conditional Operator ? :,又叫条件表达式,本文不纠结名称,统一使用条件表达式。ide
条件表达式的基本用法很是简单,它由三个操做数的运算符构成,形式为:单元测试
<表达式 1>?<表达式 2>:<表达式 3>
复制代码
条件表达式的计算从左往右计算,首先须要计算计算表达式 1 ,其结果类型必须为 Boolean
或 boolean
,不然发生编译错误。
当表达式 1 的结果为 true
,将会执行表达式 2,不然将会执行表达式 3。
表达式 2 与表达式 3 最后的类型必须得有返回结果,即不能为是 void
,若为 void
,编译时将会报错。
最后须要注意的是,表达式 2 与表达式 3 不会被同时执行,二者只有一个会被执行。
了解完三目运算符的基本原理,咱们简化一下开头例子,复现一下三目运算符使用过程的一些坑。假设咱们的例子简化成以下:
boolean flag = true; //设置成true,保证表达式 2 被执行
int simpleInt = 66;
Integer nullInteger = null;
复制代码
第一个案例咱们根据以下计算 result
的值。
int result = flag ? nullInteger : simpleInt;
复制代码
这个案例为开头的例子的简化版本,运算上述代码,将会发生 NPE 的。
为何会发发生 NPE 呢?
这里能够给你们一个小技巧,当咱们从代码上没办法找到答案时,咱们能够试试查看一下编译以后字节码,或许是 Java 编译以后增长某些东西,从而致使问题。
使用 javap -s -c class
查看 class 文件字节码,以下:
能够看到字节码中加入一个拆箱操做,而这个拆箱只有可能发生在 nullInteger
。
那么为何 Java 编译器在编译时会对表达式进行拆箱?难道全部数字类型的包装类型都会进行拆箱吗?
条件表达式表达式发生自动拆箱,其实官方在 「The Java Language Specification(简称:JLS)」15.25 节中作出一些规定,部份内容以下:
JDK7 规范
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.
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.
用大白话讲,若是表达式 2 与表达式 3 类型相同,那么这个不用任何转换,条件表达式表达式结果固然与表达式 2,3 类型一致。
当表达 2 或表达式 3 其中任一一个是基本数据类型,好比 int
,而另外一个表达式类型为包装类型,好比 Integer
,那么条件表达式表达式结果类型将会为基本数据类型,即 int
。
ps:有没有疑问?为何不规定最后结果类型都为包装类那?
这是 Java 语言层面一种规范,可是这个规范若是强制让程序员执行,想必日常使用三目运算符将会比较麻烦。因此面对这种状况, Java 在编译器在编译过程加入自动拆箱进制。
因此上述代码能够等同于下述代码:
int result = flag ? nullInteger.intValue() : simpleInt;
复制代码
若是咱们一开始的代码如上所示,那么这里错误点其实就很明显了。
接下来咱们在第一个案例基础上修改一下:
boolean flag = true; //设置成true,保证表达式 2 被执行
int simpleInt = 66;
Integer nullInteger = null;
Integer objInteger = Integer.valueOf(88);
int result = flag ? nullInteger : objInteger;
复制代码
运行上述代码,依然会发生 NPE 的问题。固然此次问题发生点与上一个案例不同,可是错误缘由倒是同样,仍是由于自动拆箱机制致使。
这一次表达式 2 与表达式 3 都为包装类 Integer
,因此条件表达式的最后结果类型也会是 Integer
。
可是因为 result
是 int 基本数据类型,好家伙,数据类型不一致,编译器将会对条件表达式的结果进行自动拆箱。因为结果为 null
,自动拆箱将报错了。
上述代码等同为:
int result = (flag ? nullInteger : objInteger).intValue();
复制代码
咱们再稍微改造一下案例 1 的例子,以下所示:
boolean flag = true; //设置成true,保证表达式 2 被执行
int simpleInt = 66;
Integer nullInteger = null;
Integer result = flag ? nullInteger : simpleInt;
复制代码
案例 3 与案例 1 右边部分彻底相同,只不过左边部分的类型不同,一个为基本数据类型 int
,一个为 Integer
。
按照案例 1 的分析,这个也会发生 NPE 问题,缘由与案例 1 同样。
这个之因此拿出来,其实想说下,上述条件表达式的结果为 int
类型,而左边类型为 Integer
,因此这里将会发生自动装箱操做,将 int
类型转化为 Integer
。
上述代码等同为:
Integer result = Integer.valueOf(flag ? nullInteger.intValue() : simpleInt);
复制代码
最后一个案例,与上面案例都不同,代码以下:
boolean flag = true; //设置成true,保证表达式 2 被执行
Integer nullInteger = null;
Long objLong = Long.valueOf(88l);
Object result = flag ? nullInteger : objLong;
复制代码
运行上述代码,依然将会发生 NPE 的问题。
这个案例表达式 2 与表达式 3 类型不同,一个为 Integer
,一个为 Long
,可是这两个类型都是 Number
的子类。
面对上述状况,JLS 规定:
Otherwise, binary numeric promotion (§5.6.2) is applied to the operand types, and the type of the conditional expression is the promoted type of the second and third operands.
Note that binary numeric promotion performs value set conversion (§5.1.13) and may perform unboxing conversion (§5.1.8).
大白话讲,当表达式 2 与表达式 3 类型不一致,可是都为数字类型时,低范围类型将会自动转为高范围数据类型,即向上转型。这个过程将会发生自动拆箱。
Java 中向上转型并不须要添加任何转化,可是向下转换必须强制添加类型转换。
上述代码转化比较麻烦,咱们先从字节码上来看:
第一步,将 nullInteger
拆箱。
第二步,将上一步的值转为 long
类型,即 (long)nullInteger.intValue()
。
第三步,因为表达式 2 变成了基本数据类型,表达式 3 为包装类型,根据案例 1 讲到的规则,包装类型须要转为基本数据类型,因此表达式 3 发生了拆箱。
第四步,因为条件表达式最后的结果类型为基本数据类型:long
,可是左边类型为 Object
,这里就须要把 long
类型装箱转为包装类型。
因此最后代码等同于:
Object result = Long.valueOf(flag ? (long)nullInteger.intValue() : objLong.longValue());
复制代码
看完上述四个案例,想必你们应该会有种感觉,没想到这么简单的条件表达式,既然暗藏这么多「杀机」。
不过你们也不用过分惧怕,不使用条件表达式。只要咱们在开发过程重点注意包装类型的自动拆箱问题就行了,另外也要注意条件表达式的计算结果再赋值的时候自动拆箱引起的 NPE 的问题。
最好你们在开发过程当中,都遵照必定的规范,即保持表达式 2 与表达式 3 的类型一致,不让 Java 编译器有自动拆箱的机会。
建议你们没事常常看下阿里出品的『Java 开发手册』,在最新的「泰山版」就增长条件表达式的这一节规范。
ps:公号消息回复:『开发手册』,获取最新版的 Java 开发手册。
最后必定要作好的单元测试,不要惯性思惟,以为这么简单的一个东西,看起来根本不可能出错的。
欢迎关注个人公众号:程序通事,得到平常干货推送。若是您对个人专题内容感兴趣,也能够关注个人博客:studyidea.cn