最近,阿粉在一个业务改造中,使用三目运算符重构了业务代码,没想到测试的时候竟然发生 NPE 的问题。
创新互联建站是专业的乌海网站建设公司,乌海接单;提供网站建设、网站设计,网页设计,网站设计,建网站,PHP网站建设等专业做网站服务;采用PHP框架,可快速的进行乌海网站开发网页制作和功能扩展;专业做搜索引擎喜爱的网站,专业的做网站团队,希望更多企业前来合作!
重构代码非常简单,代码如下:
- // 方法返回参数类型为 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 问题。
但是我明明为 simpleObj做过判空判断,simpleObj 对象肯定不是 null,那么只有 simpleObj#getCode 返回为 null。但是我的代码并没有对这个方法返回值做任何操作,为何会触发 NPE?
难道是又是自动拆箱导致的 NPE 问题?
在解答这个问题之前,我们首先复习一下三目运算符。
三目运算符
三目运算符,官方英文名称:Conditional Operator ? :,中文直译条件表达式,本文不纠结名称,统一使用三目运算符。
三目运算符的基本用法非常简单,它由三个操作数的运算符构成,形式为:
- <表达式 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;
案例 1
第一个案例我们根据如下计算 result 的值。
- int result = flag ? nullInteger : simpleInt;
这个案例为开头的例子的简化版本,运算上述代码,将会发生 NPE 的。
为什么会发发生 NPE 呢?
这里可以给大家一个小技巧,当我们从代码上没办法找到答案时,我们可以试试查看一下编译之后字节码,或许是 Java 编译之后增加某些东西,从而导致问题。
使用 javap -s -c class 查看 class 文件字节码,如下:
可以看到字节码中加入一个拆箱操作,而这个拆箱只有可能发生在 nullInteger。
那么为什么 Java 编译器在编译时会对表达式进行拆箱?难道所有数字类型的包装类型都会进行拆箱吗?
三目运算符表达式发生自动拆箱,其实官方在 「The Java Language Specification(简称:JLS)」15.25 节[1]中做出一些规定,部分内容如下:
用大白话讲,如果表达式 2 与表达式 3 类型相同,那么这个不用任何转换,三目运算符表达式结果当然与表达式 2,3 类型一致。
当表达 2 或表达式 3 其中任一一个是基本数据类型,比如 int,而另一个表达式类型为包装类型,比如 Integer,那么三目运算符表达式结果类型将会为基本数据类型,即 int。
这是 Java 语言层面一种规范,但是这个规范如果强制让程序员执行,想必平常使用三目运算符将会比较麻烦。所以面对这种情况, Java 在编译器在编译过程加入自动拆箱进制。
所以上述代码可以等同于下述代码:
- int result = flag ? nullInteger.intValue() : simpleInt;
如果我们一开始的代码如上所示,那么这里错误点其实就很明显了。
案例 2
接下来我们在第一个案例基础上修改一下:
- 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();
案例 3
我们再稍微改造一下案例 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);
案例 4
最后一个案例,与上面案例都不一样,代码如下:
- 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 规定:
大白话讲,当表达式 2 与表达式 3 类型不一致,但是都为数字类型时,低范围类型将会自动转为高范围数据类型,即向上转型。这个过程将会发生自动拆箱。
上述代码转化比较麻烦,我们先从字节码上来看:
第一步,将 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 开发手册。
最后一定要做好的单元测试,不要惯性思维,觉得这么简单的一个东西,看起来根本不可能出错的。
参考资料
[1]15.25 节: https://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.25
[2]§5.6.2: https://docs.oracle.com/javase/specs/jls/se7/html/jls-5.html#jls-5.6.2
[3]§5.1.13: https://docs.oracle.com/javase/specs/jls/se7/html/jls-5.html#jls-5.1.13
[4]§5.1.8: https://docs.oracle.com/javase/specs/jls/se7/html/jls-5.html#jls-5.1.8
[5]Java 开发手册》解读:三目运算符为何会导致 NPE?: https://developer.aliyun.com/article/758784
标题名称:这么简单的三目运算符竟然也有这么多坑?
URL分享:http://www.csdahua.cn/qtweb/news21/22871.html
网站建设、网络推广公司-快上网,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 快上网