0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

三元表达式引发的空指针问题分析

jf_ro2CN3Fa 来源:飞天小牛肉 2023-12-06 14:39 次阅读

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

知识回顾

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

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

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

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

装箱:将基本数据类型转换成包装类(每个包装类的构造方法都可以接收各自数据类型的变量)。

拆箱:从包装类之中取出被包装的基本类型数据(使用包装类的 xxxValue 方法)。

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

Integerobj=newInteger(10);//装箱
inttemp=obj.intValue();//拆箱

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

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

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

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

d3da2764-93d3-11ee-939d-92fbcf53809c.png

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

Integerobj=Integer.valueOf(10);
inttemp=obj.intValue();

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

d3e5a77e-93d3-11ee-939d-92fbcf53809c.png

问题重现

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

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

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

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

Exceptioninthread"main"java.lang.NullPointerException

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

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

booleanflag=true;
booleansimpleBoolean=false;
BooleannullBoolean=null;

booleanx=flag?nullBoolean.booleanValue():simpleBoolean;

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

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

原理分析

关于为什么编辑器会在代码编译阶段对于三目运算符中的表达式进行自动拆箱,其实在《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:

d405467e-93d3-11ee-939d-92fbcf53809c.png

看我框出来的两句话:

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

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

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

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

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

booleanflag=true;
booleansimpleBoolean=false;
BooleanobjectBoolean=Boolean.FALSE;

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

Booleanx1=flag?objectBoolean:objectBoolean;

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

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

booleanx2=flag?simpleBoolean:simpleBoolean;

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

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

booleanx3=flag?objectBoolean:simpleBoolean;

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

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

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

booleanx4=flag?objectBoolean:objectBoolean;

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

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

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

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

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

booleanflag=true;
booleansimpleBoolean=false;
BooleanobjectBoolean=Boolean.FALSE;
//将包装类设置为null
BooleannullBoolean=null;

booleanx3=flag?nullBoolean:simpleBoolean;
booleanx4=flag?nullBoolean:objectBoolean;
Booleanx6=flag?nullBoolean:simpleBoolean;

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

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

而 x4 是因为三目运算符运算结束后根据规则得到的是一个对象类型,但是在给变量赋值过程中进行自动拆箱所导致的 NPE。

审核编辑:黄飞

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • JAVA
    +关注

    关注

    19

    文章

    2904

    浏览量

    102995
  • 指针
    +关注

    关注

    1

    文章

    473

    浏览量

    70363
  • 编译器
    +关注

    关注

    1

    文章

    1577

    浏览量

    48618
  • JDK
    JDK
    +关注

    关注

    0

    文章

    77

    浏览量

    16489

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

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    什么是正则表达式?正则表达式如何工作?哪些语法规则适用正则表达式

    实现自动化文本处理。在许多编程语言中,正则表达式都被广泛用于文本处理、数据分析、网页抓取等领域。通过正则表达式,我们可以精确地筛选、操作和格式化文本,提高工作效率。
    的头像 发表于 11-03 14:41 568次阅读
    什么是正则<b class='flag-5'>表达式</b>?正则<b class='flag-5'>表达式</b>如何工作?哪些语法规则适用正则<b class='flag-5'>表达式</b>?

    C语言:指针表达式

    结果的类型是指向字符的指针指针。同样,这个值的存储位置并未清晰定义,所以这个表达式不是一个合法的左值。验证:#includeint main(){ char ch = 'a'; char *cp
    发表于 01-11 13:41

    如何创建正则的表达式

    正则表达式:用于匹配规律规则的表达式,正则表达式最初是科学家对人类神经系统的工作原理的早期研究,现在在编程语言中有广泛的应用,经常用于表单校验,高级搜索等。
    发表于 10-27 15:49

    基因表达式编程的2种解码方法

    在基因表达式编程的基础上提出2种新的解码方法,分析了它们的时间和空间复杂度。第1种方法完全遵照原始基因表达式编程中基因型与表现型之间的映射关系,直接在基因型上计算
    发表于 04-10 09:00 19次下载

    防范表达式的失控

    在C 语言中,表达式是最重要的组成部分之一,几乎所有的代码都由表达式构成。表达式的使用如此广泛,读者也许会产生这样的疑问,像+ 、- 、3 、/ 、& & 这样简单的运算也会出现
    发表于 04-22 16:57 13次下载

    C语言指针表达式实例程序说明

    本文档的主要内容详细介绍的是C语言指针表达式实例程序说明。
    发表于 11-05 17:07 4次下载
    C语言<b class='flag-5'>指针</b>的<b class='flag-5'>表达式</b>实例程序说明

    Python正则表达式指南

    本文介绍了Python对于正则表达式的支持,包括正则表达式基础以及Python正则表达式标准库的完整介绍及使用示例。本文的内容不包括如何编写高效的正则表达式、如何优化正则
    发表于 03-26 09:13 10次下载
    Python正则<b class='flag-5'>表达式</b>指南

    C语言复杂表达式指针高级应用

    应用。一、指针数组与数组指针1、字面意思来理解指针数组与数组指针(1)指针数组的实质是一个数组,这个数组中存储的内容全部是
    发表于 01-13 14:27 4次下载
    C语言复杂<b class='flag-5'>表达式</b>与<b class='flag-5'>指针</b>高级应用

    Lambda表达式详解

    C++11中的Lambda表达式用于 **定义并创建匿名的函数对象** ,以简化编程工作。下面看一下Lambda表达式的基本构成。
    的头像 发表于 02-09 11:28 849次阅读

    表达式与逻辑门之间的关系

    逻辑表达式是指表示一个表示逻辑运算关系的式子,是一个抽象的类似数学表达式,下面我们重点说明下其表达式与逻辑门之间的关系。
    的头像 发表于 02-15 14:54 1112次阅读
    <b class='flag-5'>表达式</b>与逻辑门之间的关系

    C语言的表达式

    在C语言中,表达式是由操作符和操作数组成。表达式可以由一个或者多个操作数组成,不同的操作符与操作数组成不同的表达式,因此,表达式才是C语言的基本。
    的头像 发表于 02-21 15:09 952次阅读
    C语言的<b class='flag-5'>表达式</b>

    逻辑运算符与表达式

    在C语言中,我们通常会进行真值与假值的判断,这时我们就需要用到逻辑运算符与逻辑表达式。如果表达式的值不为0,则通通返回为真值。只有当表达式的值为0时,才会返回假值。
    的头像 发表于 02-21 15:16 1442次阅读
    逻辑运算符与<b class='flag-5'>表达式</b>

    一文详解Verilog表达式

    表达式由操作符和操作数构成,其目的是根据操作符的意义得到一个计算结果。表达式可以在出现数值的任何地方使用。
    的头像 发表于 05-29 16:23 1938次阅读
    一文详解Verilog<b class='flag-5'>表达式</b>

    zabbix触发器表达式 基本RS触发器表达式 rs触发器的逻辑表达式

    zabbix触发器表达式 基本RS触发器表达式 rs触发器的逻辑表达式  Zabbix是一款开源的监控软件,它能通过监控指标来实时监测服务器和网络的运行状态,同时还能提供警报和报告等功能来帮助管理员
    的头像 发表于 08-24 15:50 1213次阅读

    怎么去选择使用gm的三种表达式呢?

    我们在写跨导gm的表达式时,知道gm有三种表达式表达式含有的变量其实只有三个,一个W/L,一个Vgs-Vth,还有一个Id。
    的头像 发表于 09-17 15:31 3085次阅读
    怎么去选择使用gm的三种<b class='flag-5'>表达式</b>呢?