这么简单的三目运算符,竟然这么多坑?

最近在一个业务改造中,使用三目运算符重构了业务代码,没想到测试的时候竟然发生 NPE 的问题。

重构代码非常简单,代码如下:

// 方法返回参数类型为 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 ,其结果类型必须为 Booleanboolean,否则发生编译错误。

当表达式 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]中做出一些规定,部分内容如下:

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;

如果我们一开始的代码如上所示,那么这里错误点其实就很明显了。

案例 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 规定:

Otherwise, binary numeric promotion (§5.6.2[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[3]) and may perform unboxing conversion (§5.1.8[4]).

大白话讲,当表达式 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 开发手册。

最后一定要做好的单元测试,不要惯性思维,觉得这么简单的一个东西,看起来根本不可能出错的。

参考资料

[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


往期推荐

面试官不讲武德,竟然问了我18个JVM问题!


Java中不可或缺的59个小技巧,贼好用!


2万字,看完这篇才敢说自己真的懂线程池!


关注我,每天陪你进步一点点!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/545197.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java 邮箱判断 正则表达式

import java.util.Scanner;public final class EmailCheck {public static boolean checkEmail(String email){String regex1 "[a-zA-Z][a-zA-Z0-9_]*[a-zA-Z0-9][.][a-zA-Z0-9]";//字母开头&#xff0c;后加字母或数字&#xff0c;后面加点&#xff0c;后面字母或数…

Java DataInputStream skipBytes()方法与示例

DataInputStream类skipBytes()方法 (DataInputStream Class skipBytes() method) skipBytes() method is available in java.io package. skipBytes()方法在java.io包中可用。 skipBytes() method is used to skip the given number of bytes of data from this DataInputStrea…

nagios客户端nrped服务方式启动脚本

1、平时配置nagios客户端nrped启动最常用的就是在/etc/rc.local文件配置&#xff1a;/usr/local/nagios/bin/nrpe -c /usr/local/nagios/etc/nrpe.cfg -d2、但是还有更好的方式&#xff08;这样方便使用脚本启动或者关闭&#xff09;&#xff1a;在/etc/init.d目录下创建nrped脚…

用好MySQL的21个好习惯!

前言每一个好习惯都是一笔财富&#xff0c;本文分SQL后悔药&#xff0c; SQL性能优化&#xff0c;SQL规范优雅三个方向&#xff0c;分享写SQL的21个好习惯&#xff0c;谢谢阅读&#xff0c;加油哈~1. 写完SQL先explain查看执行计划&#xff08;SQL性能优化&#xff09;日常开发…

第四章图像增强

第四章图像增强1_图像增强的概念2_空间域增强2.1_图像增强的点运算2.1.1_灰度变换2.1.2_直方图均衡化2.1.3 直方图规定化1_图像增强的概念 什么是图像增强&#xff1a;图像增强是采用一系列技术去改善图像的视觉效果,或将图像转换成一种更适合于人或机器进行分析和处理的形式。…

Java DataInputStream readShort()方法(带示例)

DataInputStream类readShort()方法 (DataInputStream Class readShort() method) readShort() method is available in java.io package. readShort()方法在java.io包中可用。 readShort() method is used to read 2 bytes (i.e. 16 bit) of data input and returns a short va…

IP 对应 网址

IP 对应 网址 /* 网址和IP对应的小例子 */try {InetAddress address_1 InetAddress.getByName("www.baidu.com");InetAddress address_2 InetAddress.getByName("10.2.8.13");System.out.println(address_1.toString());System.out.println(address_2.t…

springboot发送qq邮件

springboot发送qq邮件1_开启邮箱相关权限并获取邮箱授权码2_实现功能2.1_添加mail的依赖2.1.1_创建工程时添加2.1.2_在工程中添加2.2_配置文件application.properties配置相关信息2.3_实现代码1_开启邮箱相关权限并获取邮箱授权码 进入账户 开启POP3/SMTP服务并生成授权码 …

反转链表-剑指offer-16

题目&#xff1a;定义一个函数&#xff0c;输入一个链表的头节点&#xff0c;反转该链表并输出反转后链表的头节点。分析&#xff1a;逐个头插&#xff0c;实现反转设置3个指针&#xff1a;head 头节点、prev 前一个节点、 cur 下一个节点注意&#xff1a;链表为空&#xff0c;…

getsimplename_Java类类getSimpleName()方法的示例

getsimplename类类getSimpleName()方法 (Class class getSimpleName() method) getSimpleName() method is available in java.lang package. getSimpleName()方法在java.lang包中可用。 getSimpleName() method is used to return the simple name of the underlying class as…

MyEclipse编码设置,中文乱码解决方法,UTF-8,GBK

在Myeclipse导入一个项目&#xff0c;有中文乱码问题&#xff0c;解决方法如下&#xff1a;一、将整个project设置编码UTF-8&#xff08;UTF-8可以最大的支持国际化&#xff09; windows->Preferences->general->Workspace->Text file encoding->Other框中的Tex…

Spring Cloud Alibaba 深度解密!

说说吧&#xff0c;程序猿们&#xff0c;你们还有谁不是“单身G”&#xff1f;想要“赢取”白富美&#xff0c;当上CTO&#xff0c;走上人生巅峰&#xff0c;不努力怎么可以&#xff1f;别人疯狂购物&#xff0c;你疯狂学习&#xff0c;努力30天&#xff0c;向阿里P6迈进&#…

安卓连接真机调试

安卓连接真机调试一、打开开发者模式二、打开USB调试三、最后连接数据线这里使用荣耀20pro为例一、打开开发者模式 点击版本号&#xff0c;多点几下直到打开开发者模式 二、打开USB调试 系统与更新——>开发人员选项 三、最后连接数据线 连接数据线并选择MIDI模式

Java ByteArrayInputStream skip()方法与示例

ByteArrayInputStream类skip()方法 (ByteArrayInputStream Class skip() method) skip() method is available in java.util package. skip()方法在java.util包中可用。 skip() method is used to skip the given number of bytes (no_of_bytes) from this stream. skip()方法用…

3W字!带你玩转「消息队列」

1. 消息队列解决了什么问题消息中间件是目前比较流行的一个中间件&#xff0c;其中RabbitMQ更是占有一定的市场份额&#xff0c;主要用来做异步处理、应用解耦、流量削峰、日志处理等等方面。1. 异步处理一个用户登陆网址注册&#xff0c;然后系统发短信跟邮件告知注册成功&…

okhttp_utils的使用以及与服务端springboot交互中遇到的问题

okhttp_utils的使用以及与服务端springboot交互中遇到的问题1_okhttp_utils在Android studio中的引入方法2_okhttputils的使用举例3_get和post的简单使用3_图片的上传3.1_单张图片的上传3.1.1_获取安卓本地图片问题3.1.2_okhttputils上传图片代码3.1.3_服务端接收图片3.2_单张图…

算法系列之图--DFS

深度优先搜索使用的策略是&#xff0c;只要与可能就在图中尽量“深入”。DFS总是对最近才发现的结点v出发边进行探索&#xff0c;知道该结点的所有出发边都被发现为止。一旦v的所有出发边都被发现了&#xff0c;搜索就回溯到v的前驱结点&#xff08;v是经该结点才被发现的&…

python列表中随机选择_如何在Python中从列表中随机选择一个项目?

python列表中随机选择Python random module provides an inbuilt method choice() has an ability to select a random item from the list and other sequences. Using the choice() method, either a single random item can be chosen or multiple items. The below set of …

这8种常见的SQL错误用法,你还在用吗?

来源 | yq.aliyun.com/articles/72501MySQL 在近几年仍然保持强劲的数据库流行度增长趋势。越来越多的客户将自己的应用建立在 MySQL 数据库之上&#xff0c;甚至是从 Oracle 迁移到 MySQL上来。但也存在部分客户在使用 MySQL 数据库的过程中遇到一些比如响应时间慢&#xff0c…

微软披露了Spartan中所使用的渲染引擎的细节

微软披露了在Spartan web浏览器中所使用的新渲染引擎的更多信息&#xff0c;Windows 10的桌面版本和移动设备版本将预装该浏览器。\\Charles Morris是Spartan项目的项目经理主管&#xff0c;他在一篇博客帖子中详细地解释了该项目背后的成因、IE浏览器的历史以及未来的计划。该…