三目运算符导致NPE问题

在三目运算符中,表达式 1 和 2 在涉及算术计算或数据类型转换时,会触发自动拆箱。当其中的操作数为 null 值时,会导致 NPE 。

三目运算符是 Java 语言中的重要组成部分,它也是唯一有 3 个操作数的运算符。形式为:

<表达式1> ? <表达式2> : <表达式3>

以上,通过 ?、:  组合的形式得到一个条件表达式。其中 ? 运算符的含义是:先求表达式 1 的值,如果为真,则执行并返回表达式 2 的结果;如果表达式 1 的值为假,则执行并返回表达式 3 的结果。

 

值得注意的是,一个条件表达式从不会既计算 <表达式 2>,又计算 <表达式 3>。条件运算符是右结合的,也就是说,从右向左分组计算。例如,a?b:c?d:e 将按 a?b:(c?d:e) 执行。

自动装箱与自动拆箱

 

介绍过了三目运算符(条件表达式)之后,我们再来简单介绍下 Java 中的自动拆装箱相关知识点。

每一个 Java 开发者一定都对 Java 中的基本数据类型不陌生,Java 中共有 8 种基本数据类型,这些基础数据类型带来一个好处就是他们直接在栈内存中存储,不会在堆上分配内存,使用起来更加高效。

但是,Java 语言是一个面向对象的语言,而基本数据类型不是对象,导致在实际使用过程中有诸多不便,如集合类要求其内部元素必须是 Object 类型,基本数据类型就无法使用。

所以,相对应的,Java 提供了 8 种包装类型,更加方便在需要对象的地方使用。
有了基本数据类型和包装类,带来了一个麻烦就是需要在他们之间进行转换。在 Java SE5 中,为了减少开发人员的工作,Java 提供了自动拆箱与自动装箱功能。

自动装箱:就是将基本数据类型自动转换成对应的包装类。

自动拆箱:就是将包装类自动转换成对应的基本数据类型。

Integer i =10;  //自动装箱
int b= i; //自动拆箱

自动装箱都是通过包装类的 valueOf() 方法来实现的.自动拆箱都是通过包装类对象的xxxValue() 来实现的(如 booleanValue()、longValue() 等)。

 

boolean flag = true; //设置成true,保证条件表达式的表达式二一定可以执行

boolean simpleBoolean = false; //定义一个基本数据类型的boolean变量

Boolean nullBoolean = null;//定义一个包装类对象类型的Boolean变量,值为null 

boolean x = flag ? nullBoolean : simpleBoolean; //使用三目运算符并给x变量赋值

以上代码,在运行过程中,会抛出 NPE:

而且,这个和你使用的 JDK 版本是无关的,作者分别在 JDK 6、JDK 8 和 JDK 14 上做了测试,均会抛出 NPE。因为Boolean会自动拆箱调用booleanValue()导致NPE

简单的来说就是:当第二位和第三位操作数的类型相同时,则三目运算符表达式的结果和这两位操作数的类型相同。当第二,第三位操作数分别为基本类型和该基本类型对应的包装类型时,那么该表达式的结果的类型要求是基本类型。

 为了满足以上规定,又避免程序员过度感知这个规则,所以在编译过程中编译器如果发现三目操作符的第二位和第三位操作数的类型分别是基本数据类型(如 boolean)以及该基本类型对应的包装类型(如 Boolean)时,并且需要返回表达式为包装类型,那么就需要对该包装类进行自动拆箱。

其实简单总结下,就是:

当第二位和第三位表达式都是包装类型的时候,该表达式的结果才是该包装类型,否则,只要有一个表达式的类型是基本数据类型,则表达式得到的结果都是基本数据类型。如果结果不符合预期,那么编译器就会进行自动拆箱。即 Java 开发手册中总结的:只要表达式 1 和表达式 2 的类型有一个是基本类型,就会做触发类型对齐的拆箱操作,只不过如果都是基本类型也就不需要拆箱了。

小结

最好的做法就是保持三目运算符的第二位和第三位表达式的类型一致,并且如果要把三目运算符表达式给变量赋值的时候,也尽量保持变量的类型和他们保持一致。并且,做好单元测试!!!

扩展思考

为了方便大家理解,我使用了简单的布尔类型的例子说明了 NPE 的问题。但是实际在代码开发中,遇到的场景可能并没有那么简单,比如说以下代码,大家猜一下能否正常执行:

Map<String,Boolean> map =  new HashMap<String, Boolean>();Boolean b = (map!=null ? map.get("Hollis") : false);

 如果你的答案是"不能,这里会抛 NPE"那么说明你看懂了本文的内容,但是,我只能说你只是答对了一半。
因为以上代码,在小于 JDK 1.8 的版本中执行的结果是 NPE,在 JDK 1.8 及以后的版本中执行结果是 null。

因为 Boolean b = (map!=null ? map.get("Hollis") : false);  表达式中,第二位操作数为 map.get("test") ,虽然 Map 在定义的时候规定了其值类型为 Boolean,但是在编译过程中泛型是会被擦除的(泛型的类型擦除),所以,其结果就是 Object。那么根据以上规则判断,这个表达式就是引用表达式。

那么以上代码在 Java 8 中反编译后内容如下:

Boolean b = maps == null ? Boolean.valueOf(false) : (Boolean)maps.get("Hollis");

但是在 Java 7 中可没有这些规定(Java 8 之前的类型推断功能还很弱),编译器只知道表达式的第二位和第三位分别是基本类型和包装类型,而无法推断最终表达式类型。
那么他就会先根据 JLS 15.25 的规定,把返回值结果转换成基本类型。然后在进行变量赋值的时候,再转换成包装类型:

 

Boolean b = Boolean.valueOf(maps == null ? false : ((Boolean)maps.get("Hollis")).booleanValue());

所以,相比 Java 8 中多了一步自动拆箱,所以会导致 NPE。

参考资料:
《Java 开发手册(泰山版)》
posted @ 2020-05-08 10:04  ruanyc  阅读(0)  评论(0)    收藏  举报