当前位置: 首页 > 科技观察

这么简单的三元运算符竟然有这么多坑?

时间:2023-03-21 13:30:31 科技观察

最近阿粉在一次业务转型中使用三元运算符重构业务代码。没想到在测试的时候出现了NPE问题。重构代码很简单,代码如下://方法返回参数类型为Integer//privateIntegercode;SimpleObjsimpleObj=newSimpleObj();//其他业务逻辑if(simpleObj==null){return-1;}else{returnsimpleObj.getCode();}这个if判断,阿芬看到的时候觉得很繁琐,于是用三元运算符重构,代码如下://方法的返回参数类型为IntegerSimpleObjsimpleObj=newSimpleObj();//其他业务逻辑返回impleObj==null?-1:simpleObj.getCode();在测试过程中,第四行代码抛出一个空指针。这里的代码非常简单。显然,只有simpleObj#getCode会导致NPE问题。但是我明明对simpleObj做了null判断,而simpleObj对象肯定不为null,所以只有simpleObj#getCode返回null。但是我的代码没有对这个方法的返回值做任何事情。为什么会触发NPE?难道又是自动拆箱导致的NPE问题?在回答这个问题之前,我们先来回顾一下三元运算符。三目算子三目算子,官方英文名称:ConditionalOperator?:,中文直译条件表达式,本文不纠结名字,统一使用三元运算符。三元运算符的基本用法很简单,它由三个操作数的运算符组成,形式为:<表达式1>?<表达式2>:<表达式3>三元运算符从左到右计算。首先需要对表达式1进行计算,结果类型必须是Boolean或boolean,否则会出现编译错误。当表达式1的计算结果为真时,将执行表达式2,否则将执行表达式3。表达式2和表达式3的final类型必须有返回结果,即不能为void。如果为void,编译时会报错。最后需要注意的是,表达式2和表达式3不会同时执行,只会执行其中一个。踩坑案例了解了三元运算符的基本原理后,我们来简化初始示例,重现三元运算符使用中的一些陷阱。假设我们的例子简化如下:booleanflag=true;//设置为true保证表达式2被执行intsimpleInt=66;IntegernullInteger=null;情况1在第一种情况下,我们按如下方式计算result的值。intresult=flag?nullInteger:simpleInt;此案例是第一个示例的简化版本。执行上述代码时,会出现NPE。为什么会发生NPE?这里给大家一个小技巧。当我们从代码中找不到答案时,可以尝试编译后查看字节码。可能是Java编译后加了东西,导致疑问。使用javap-s-cclass查看class文件的字节码,如下:可以看到在字节码中添加了一个拆箱操作,而这个拆箱只能发生在nullInteger上。那么为什么Java编译器在编译的时候会把表达式拆箱呢?numeric类型的所有包装类型都unbox吗?三元运算符表达式会自动取消装箱。事实上,官方的《TheJavaLanguageSpecification(简称:JLS)》Section15.25[1]做了一些规定,其中的一些规定如下:JDK7规范如果第二个和第三个操作数的类型相同(可能是null类型),那么这就是条件表达式的类型。如果第二个和第三个操作数之一是原始类型T,而另一个的类型是对T应用装箱转换(§5.1.7)的结果,那么条件表达式的类型就是T,如果表达式2和表达式3的类型相同,那么这个不需要任何转换,三元运算符的表达式结果当然是一致的表达式2和3的类型。当表达式2或表达式3中的任何一个是基本数据类型,例如int,另一个表达式类型是包装类型,例如Integer,则三元运算符表达式的结果类型会是一个基本的数据类型,即int。ps:你有什么疑惑吗?W为什么不指定最终结果类型是包装类?这是Java语言层面的规范,但是如果强行让程序员去实现这个规范,那么正常使用三元运算符肯定很麻烦。所以面对这种情况,Java在编译时给编译器加入了自动拆箱。所以上面的代码可以等价于下面的代码:intresult=flag?nullInteger.intValue():simpleInt;如果我们的初始代码如上所示,那么这里的错误点其实很明显。Case2接下来我们修改第一种情况:booleanflag=true;//设置为true,保证表达式2执行intsimpleInt=66;IntegernullInteger=null;IntegerobjInteger=Integer.valueOf(88);intresult=flag?nullInteger:objInteger;运行上面的代码,还是会出现NPE的问题。当然,这次的问题点和上次不同,但是错误的原因是一样的,仍然是自动拆箱机制导致的。这次,表达式2和表达式3都是包装类Integer,所以三元运算符的最终结果类型也将是Integer。但是由于result是int的基本数据类型,好家伙,数据类型不一致,编译器会自动拆箱三元运算符的结果。由于结果为null,自动拆箱会报错。上面的代码相当于:intresult=(flag?nullInteger:objInteger).intValue();Case3对Case1的例子稍微修改一下,如下:booleanflag=true;//设置为true,保证表达式2为ExecuteintsimpleInt=66;IntegernullInteger=null;Integerresult=flag?nullInteger:simpleInt;Case3和Case1右边部分完全一样,只是左边部分的类型不同,一个是基本数据类型int,一个是Integer。根据Case1分析,也会出现这个NPE问题,原因同Case1。之所以把这个拿出来是因为上面的三元运算符的结果是int类型,左边type是Integer,所以这里会发生自动装箱操作,将int类型转换成Integer。上面的代码等价于:Integerresult=Integer.valueOf(flag?nullInteger.intValue():simpleInt);案例4的最后一个案例与上面的案例不同,代码如下:booleanflag=true;//设置为true保证表达式公式2执行IntegernullInteger=null;LongobjLong=Long.valueOf(88l);Objectresult=标志?nullInteger:objLong;运行上面的代码还是会出现NPE问题。本例中,表达式2和表达式3的类型不同,一个是Integer,一个是Long,但是这两个类型都是Number的子类。面对上述情况,JLS规定:否则,对操作数类型进行二进制数值提升(§5.6.2[2]),条件表达式的类型为第二个和第三个操作数的提升后类型。请注意,二进制数字提升执行值集转换(§5.1.13[3])并可能执行拆箱转换(§5.1.8[4])。说白了,当表达式2和表达式3的类型不一致,但都是数值类型时,会自动将低位数据类型转换为高位数据类型,即向上转换。此过程将自动拆箱。Java中的向上转型不需要添加任何转换,但向下转型必须强制添加类型转换。上面的代码转换比较麻烦。我们先看字节码:第一步是对nullInteger进行拆箱。第二步,将上一步中的值转换为long类型,即(long)nullInteger.intValue()。第三步,由于表达式2已经成为基本数据类型,而表达式3是封装类型,根据案例1中提到的规则,需要将封装类型转换为基本数据类型,所以对表达式3进行拆箱。第四步,由于三元运算符最终的结果类型是基本数据类型:long,而左边的类型是Object,这里需要将long类型装箱成一个wrapper类型。所以最后的代码相当于:Objectresult=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