Redian新闻
>
重大线上事故!三元表达式引发的空指针问题…

重大线上事故!三元表达式引发的空指针问题…

公众号新闻

👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入芋道快速开发平台知识星球。下面是星球提供的部分资料: 

👉这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号等等功能:

  • Boot 地址:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 地址:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn

来源:飞天小牛肉


属实刺激,刚入职不久就遇到这种史诗级的线上 Bug,首页直接崩溃,陈年老代码爆雷,不管落到最后的底层原因是什么,我感觉主要还是上下游的链路太过复杂,治理难度比较大,牵一发而动全身。

知识回顾

三目运算符大家都很熟悉了:

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

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

再来回顾下自动拆箱和装箱机制,Java 通过这种机制使得包装类和基本数据类型之间的转换更加方便:

  • 装箱:将基本数据类型转换成包装类(每个包装类的构造方法都可以接收各自数据类型的变量)。
  • 拆箱:从包装类之中取出被包装的基本类型数据(使用包装类的 xxxValue 方法)。

下面以 Integer 为例,我们来看看 Java 内置的包装类是如何进行拆装箱的:

Integer obj = new Integer(10);  // 装箱
int temp = obj.intValue();   // 拆箱

这种形式的代码是 JDK 1.5 以前的,JDK 1.5 之后,Java 设计者为了方便开发提供了自动装箱(Autoboxing)与自动拆箱的机制,并且可以直接利用包装类的对象进行数学计算。

还是以 Integer 为例,我们来看看自动拆装箱的过程:

Integer obj = 10;   // 自动装箱. 基本数据类型 int -> 包装类 Integer
int temp = obj;   // 自动拆箱. Integer -> int
obj ++; // 直接利用包装类的对象进行数学计算
System.out.println(temp * obj); 

基本数据类型到包装类的转换,不需要像上面一样使用构造函数,直接 = 就完事儿;同样的,包装类到基本数据类型的转换,也不需要我们手动调用包装类的 xxxValue 方法了,直接 = 就能完成拆箱。这也是将它们称之为自动的原因。

我们来看看这段代码反编译后的文件,底层到底是什么原理:

Integer obj = Integer.valueOf(10);
int temp = obj.intValue();

可以看见,自动装箱的底层原理其实就是调用了包装类的 valueOf 方法,而自动拆箱的底层同样还是调用了包装类的 intValue() 方法。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

问题重现

实际的代码业务逻辑比较复杂,这里我们举一个相对简单一点的例子先来重现下这个问题:

// 设置成true,保证条件表达式的表达式二一定可以执行
boolean flag = true;
//定义一个包装类对象类型的Boolean变量,值为null 
Boolean nullBoolean = null;
// 定义一个基本数据类型的boolean变量
boolean simpleBoolean = false

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

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

Exception in thread "main" java.lang.NullPointerException

而且,这个和你使用的 JDK 版本是无关的,我在 JDK 6、JDK 8 和 JDK 14 上做了测试,均会抛出 NPE。

尝试对以上代码进行反编译,使用 jad 工具进行反编译后,得到以下代码:

boolean flag = true;
boolean simpleBoolean = false;
Boolean nullBoolean = null;

boolean x = flag ? nullBoolean.booleanValue() : simpleBoolean;

可以看到,反编译后的代码的最后一行,编译器帮我们做了一次自动拆箱(nullBoolean 是包装类,而 x 是基本类型),而 nullBoolean 是 null,这就出现了 null.booleanValue,从而抛出 NPE。

那么,为什么编译器会进行自动拆箱呢?什么情况下需要进行自动拆箱呢?

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

原理分析

关于为什么编辑器会在代码编译阶段对于三目运算符中的表达式进行自动拆箱,其实在《The Java Language Specification》(后文简称 JLS,是Java 语言规范,是一切 Java 编程的基础参照文档)的第 15.25 章节中是有相关介绍的。我们直接看 Java SE 1.7 JLS 中关于这部分的描述(因为 1.7 的表述更加简洁一些),原文地址 -> https://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.25:

看我框出来的两句话:

  1. 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. 当第二位和第三位操作数的类型相同时,则三目运算符表达式的结果和这两位操作数的类型相同。
  2. 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. 当第二,第三位操作数分别为基本类型和该基本类型对应的包装类型时,那么该表达式的结果的类型要求是基本类型。

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

理解下这句话,JLS 的规范是如果第二和第三位操作数分别是基本类型和包装类型,那么要求返回值是基本类型。那如果你自己写的代码返回值是包装类型,那么编译器为了满足 JLS 规范,其实是会自动做一个拆箱的。

简单总结:只要表达式 1 和表达式 2 的类型有一个是基本类型一个是包装类型,就会做触发类型对齐的拆箱操作。

下面再列举几个例子加深下理解:

boolean flag = true;
boolean simpleBoolean = false;
Boolean objectBoolean = Boolean.FALSE;

当第二位和第三位表达式都是包装类,表达式返回值也为包装类,编译器不需要做拆箱操作:

Boolean x1 = flag ? objectBoolean : objectBoolean;

//反编译后代码(不需要做任何特殊操作)
Boolean x1 = flag ? objectBoolean : objectBoolean;    

当第二位和第三位表达式都为基本类型时,表达式返回值也为基本类型,编译器不需要做拆箱操作:

boolean x2 = flag ? simpleBoolean : simpleBoolean;

//反编译后代码(不需要做任何特殊操作)
boolean x2 = flag ? simpleBoolean : simpleBoolean;

当第二位和第三位表达式中一个为基本类型另一个为包装类型时,表达式返回值为基本类型,编译器需要做拆箱操作:

boolean x3 = flag ? objectBoolean : simpleBoolean;

//反编译后代码(需要对其中的包装类进行拆箱)
boolean x3 = flag ? objectBoolean.booleanValue() : simpleBoolean;

如果你清楚三目运算符的规则,那你就会正确地按照以上方式去定义 x1、x2 和 x3 的类型。

但是,并不是所有人都熟知这个规则,所以在实际应用中,还会出现以下几种定义方式:

boolean x4 = flag ? objectBoolean : objectBoolean;

// 反编译后代码(三元表达式的结果要求是包装类,而 x4 是基本类型,所以编译器需要做拆箱)
boolean x4 = (flag ? objectBoolean : objectBoolean).booleanValue();
Boolean x5 = flag ? simpleBoolean : simpleBoolean;

// 反编译后代码(三元表达式的结果要求是基本类型,而 x5 是包装类型,所以编译器需要做装箱)
Boolean x5 = Boolean.valueOf(flag ? simpleBoolean : simpleBoolean);
Boolean x6 = flag ? objectBoolean : simpleBoolean;

// 反编译后代码(三元表达式的结果要求是基本类型,而 x5 是包装类型,所以编译器需要做装箱)
Boolean x6 = Boolean.valueOf(flag ? objectBoolean.booleanValue() : simpleBoolean);

所以,日常开发中就有可能出现以上 6 种情况。在以上 6 种情况中,如果是涉及到自动拆箱的,一旦包装类的值为 null,即 null.booleanValue(),就必然会发生 NPE(装箱不会,因为装箱是 Boolean.valueOf(null),这并不会抛 NPE)。

小伙伴们可以把以上的 x3、x4 以及 x6 中的的包装类设置成 null,看看是不是会抛 NPE:

boolean flag = true;
boolean simpleBoolean = false;
Boolean objectBoolean = Boolean.FALSE;
// 将包装类设置为 null
Boolean nullBoolean = null;

boolean x3 = flag ? nullBoolean : simpleBoolean;
boolean x4 = flag ? nullBoolean : objectBoolean;
Boolean x6 = flag ? nullBoolean : simpleBoolean;

以上三种情况,都会在执行时发生 NPE:

  • 其中 x3 和 x6 是三目运算符运算过程中,根据 JLS 的规则确定类型的过程中要做自动拆箱而导致的 NPE。由于使用了三目运算符,并且第二、第三位操作数分别是基本类型和对象。就需要对对象进行拆箱操作,由于该对象为 null,所以在拆箱过程中调用 null.booleanValue() 的时候就报了 NPE。
  • 而 x4 是因为三目运算符运算结束后根据规则得到的是一个对象类型,但是在给变量赋值过程中进行自动拆箱所导致的 NPE。

欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*)

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
突发 | 香港突发夺命事故!特首发话严查!这操作,挽救了一次大事故!澳华人区被曝大量交通隐患,校园周边险酿多起事故!学童安全引忧虑中国“X”老板VS.日本“X”逸翁深夜突发!马云办公室回应!三星电子掌门人又摊上事…炸裂!Chatswood被曝存在大量交通隐患!校园周边险酿多起事故!引发大批华人担忧!神秘的大杂院(十二)一朵白色的椰叶花(下)清华学霸杀妻开庭在即,华人律师说出致命问题……[电脑] Streacom VU1开箱:墨水屏+物理指针的可串联迷你监控屏​被严重警告!日本核电站又发生事故!Kylie Jenner 陷自拍事故!床头柜惊现黑色不明物体引发争议?揭秘神秘的字符串匹配工具——正则表达式指针没用好,一行代码让公司损失6000万美元最近一部电影引发的大论战,意义十分重大!清华学霸杀妻开庭推迟,华人律师说出致命问题……考上事业编!三位知名作家,被同一单位录用痛心!悉尼13岁儿童下公交时遭卡车撞伤!不治身亡!学生频频发生事故!此前还有人被撞飞!澳洲一大型工厂发生化学品泄漏事故!五人烧伤31起事故!17起烧伤!BestBuy爆款高压锅召回:看看你家是不是用这款清华学霸美国杀妻,真相让人不寒而栗:那个华人律师,说出了致命问题…尊嘟假嘟?家里面灰尘太多根本扫不净,还真是扫帚的问题……她的一张合照,没想到会以这种有趣的方式引起关注。生活里的幸运和不幸运硬核 JVM 压缩指针详解中国的沙翁,日本的沙翁频上热搜的“毒玩具”伤害身心健康?我却从中看到了更严重的问题……摔倒了,服老了悉尼华人区被曝大量交通隐患,校园周边险酿多起事故!学童安全引忧虑不规范的枚举类代码引发的一场事故无忧周报|周六有机会看到日环食,10-12月为撞鹿高发期,这些麻州城镇最多撞鹿事故!红线部份路段将停驶16天法拉盛致命事故!3岁男童被撞身亡 肇事司机弃车逃逸悬殊超114% !市场基金业绩分化大,面临这些问题……女子总觉得被跟踪,结果在车后座找出一个小圆片!澳洲政府都开始重视这个问题…本月第三次重大事故!两架波音飞机机场相撞争议大,多名乘客提起诉讼称受到伤害!波音公司:初步自检中……语雀突发 P0 级事故!宕机 8 小时被网友怒喷,运维又背锅?
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。