1.背景
大概是这样的,前不久有一个需求在dev环境联调完成之后,提测部署到test环境之后,质检员反馈竟然不能登录了,去服务器查看报错日志又是万恶的空指针错误NPE,定位到报错代码是下面的第二行代码:
css
代码解读
复制代码
user.setIsAdmin(Objects.isNull(org) ? 0 : org.getIsAdmin()); user.setIsMask(Objects.isNull(org) ? 0 : org.getIsMask());
这里我特意给出前面一行代码,是的你没看错,第一行代码没报错,但是第二行却报了空指针,看上去是不是很奇怪???
需求背景大概是这样,把一个公司层面的开关字段给到登陆上下文信息返回给前端使用,从代码上可以看到还对org
进行判空处理,因为超级管理员账号登陆不属于任何一个公司,此时org=null
,这么小心谨慎但还是出现了空指针,真是心碎一地呀~~~
那咋dev环境不会触发呢?其实原因很简单,在dev开发自测时会给org
的is_mask
写入值,这样该字段值就不会为null了,但是test环境没人写入且数据库字段默认是null的,就出现这种尴尬情况了,这在平时开发中还是蛮常见的,所以在这里强烈建议数据库字段能设置成非空的,一定要设置非空,这样有助于索引建立和避免null导致一些的隐藏问题。
其实这个问题是因为org.getIsMask()=null
,然后自动拆箱引起的,在阿里巴巴Java开发手册中就有这么一条规定:
接下来我们就来介绍下相关知识点和出现这个问题的原因所在。既然是自动拆箱引起的,那就先从它开始吧
2.自动装箱与自动拆箱
在 Java 中,自动装箱和自动拆箱是将基本数据类型与它们对应的包装类型之间相互转换的机制。Java 从 JDK 1.5 开始引入了这一特性,使基本类型和包装类型的转换更加便捷。
基本类型与包装类型的关系
Java 提供了八种基本数据类型,每种基本类型都有一个对应的包装类,主要关系如下:
基本类型 | 包装类型 |
---|---|
int | Integer |
long | Long |
double | Double |
float | Float |
char | Character |
byte | Byte |
short | Short |
boolean | Boolean |
2.1 自动装箱(Autoboxing)
自动装箱是指 Java 编译器自动将基本类型转换为相应的包装类型。例如,将 int
自动转换为 Integer
,从而可以直接将基本类型赋值给包装类型的变量。示例如下:
ini
代码解读
复制代码
int a = 5; Integer b = a; // 自动装箱,将 int 转换为 Integer
在这个例子中,a
是基本类型 int
,而 b
是包装类型 Integer
。Java 自动将 int
值 5
转换为 Integer
对象。
自动装箱其实就是使用包装类型的静态方法 valueOf()
来完成转换。例如:
ini
代码解读
复制代码
Integer b = Integer.valueOf(a);
Java 采用这种方式进行自动装箱,而不是直接创建新的 Integer
对象,因为 valueOf()
方法使用了缓存池,能提升性能并减少内存消耗。
2.3 自动拆箱(Unboxing)
自动拆箱是指 Java 编译器自动将包装类型转换为基本类型。例如,将 Integer
自动转换为 int
,从而可以直接在表达式中使用包装类型的变量。示例如下:
ini
代码解读
复制代码
Integer a = 5; // 自动装箱 int b = a; // 自动拆箱,将 Integer 转换为 int
在这个例子中,a
是包装类型 Integer
,而 b
是基本类型 int
。Java 自动将 Integer
对象 a
的值转换为 int
类型。
自动拆箱实际上是通过调用包装类中的 xxxValue()
方法完成的,例如 Integer
调用 intValue()
,Double
调用 doubleValue()
,等等。
ini
代码解读
复制代码
int b = a.intValue();
这行代码一看就嗅到了可能引发 NullPointerException
的气息~~~
都说到这里了,就再来看看缓存池机制
2.4 缓存池机制
部分包装类型(如 Integer
)对特定范围内的值使用缓存池,以提高性能。对于 Integer
,值在 -128
到 127
之间时,valueOf()
方法会返回缓存对象,而非新建对象。因此,对于 Integer
类型的值比较,可以使用 ==
在 -128
到 127
范围内进行比较,但超出该范围则应使用 equals()
方法。
ini
代码解读
复制代码
Integer a = 127; Integer b = 127; System.out.println(a == b); // true, 使用缓存池 Integer c = 128; Integer d = 128; System.out.println(c == d); // false, 超出缓存池范围
强烈推荐使用 JDK7 引入的工具类 java.util.Objects#equals(Object a, Object b)进行等值比较,BigDecimal 的等值比较应使用 compareTo() 方法,而不是 equals() 方法。因为equals() 方法会比较值和精度(1.0 与 1.00 返回结果为 false),而 compareTo() 则会忽略精度。
3.三目运算符
三目运算符的语法如下:
ini
代码解读
复制代码
result = condition ? value1 : value2;
其中:
condition
是一个布尔表达式。value1
和value2
是不同条件下的返回值。result
是value1
和value2
的推断公共类型。
从语法、使用都是相当简单,这也是大家喜欢使用的原因,但竟然会出现空指针这个大坑...
在 Java 中,三目运算符的返回类型由 value1
和 value2
的类型推断。如果返回类型是包装类(如 Integer
)且其中一个分支为 null
,而另一个分支是基本类型,Java 会尝试将 null
自动拆箱为基本类型,从而引发 NullPointerException
。
这里我们分别来看看各种情况:
3.1 结果是包装类,value1和value2一个是包装类,一个是基本类型
ini
代码解读
复制代码
boolean flag = true; Integer result; Integer value1 = null; int value2 = 8; result = flag ? value1 : value2;
运行代码,报错NPE:
arduino
代码解读
复制代码
Exception in thread "main" java.lang.NullPointerException
上面提到自动拆箱发生在编译期间,所以我们使用反编译工具jd-gui
对测试代码class文件进行反编译,得到代码如下:
ini
代码解读
复制代码
Integer value1 = null; Integer result = Integer.valueOf(1 != 0 ? value1.intValue() : 8);
可以看到,反编译后的代码的最后一行,编译器帮我们做了一次自动拆箱,而就是因为这次自动拆箱,导致代码出现对于一个 null 对象的调用,导致了 NPE。
3.2 结果是包装类,value1、value2都是基本类型
ini
代码解读
复制代码
boolean flag = true; Integer result; int value1 = 6; int value2 = 8; result = flag ? value1 : value2;
执行结果result=6
,反编译代码如下:
ini
代码解读
复制代码
int i; if (1 != 0) { i = 6; } else { i = 8; } Integer result = Integer.valueOf(i);
3.3 结果、value1、value2都是包装类
ini
代码解读
复制代码
boolean flag = true; Integer result; Integer value1 = null; Integer value2 = 8; result = flag ? value1 : value2;
执行结果result=null
,不会报错,反编译代码如下:
ini
代码解读
复制代码
Integer num; if (1 != 0) { num = null; } else { num = 8; } Integer result = num;
3.4 结果、value1、value2都是基本类型
ini
代码解读
复制代码
boolean flag = true; int result; int value1 = 6; int value2 = 8; result = flag ? value1 : value2;
执行结果result=6
,这种肯定不会报错,基本类型不会有null这种情况,自然就没有了空指针这一说,至于反编译代码和 3.2小结的反编译代码差不多。
3.5 结果是基本类型,value1和value2是包装类型
ini
代码解读
复制代码
boolean flag = true; int result; Integer value1 = null; Integer value2 = 8; result = flag ? value1 : value2;
运行结果报错NPE,反编译的代码如下:
ini
代码解读
复制代码
Integer num; if (1 != 0) { num = null; } else { num = 8; } int result = num.intValue();
num
为null,肯定报空指针
3.6 结果是基本类型,value1和value2一个是包装类,一个是基本类型
ini
代码解读
复制代码
boolean flag = true; int result; Integer value1 = null; int value2 = 8; result = flag ? value1 : value2;
执行结果报错NPE,反编译代码如下:
ini
代码解读
复制代码
Integer value1 = null; int result = 1 != 0 ? value1.intValue() : 8;
结论:只要三个对象类型不一样,并发生自动拆箱时对象为null,就会发生空指针NPE.
项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用
Github地址:github.com/plasticene/…
Gitee地址:gitee.com/plasticene3…
微信公众号:Shepherd进阶笔记
交流探讨qun:Shepherd_126
4.解决方案
为避免空指针异常,可以采取以下策略:
确保相同类型:在三目运算符中使用相同的包装类型或基本类型,避免混合类型推断导致的自动拆箱。
ini
代码解读
复制代码
Integer a = null; Integer result = (a != null) ? a : Integer.valueOf(0);
使用if-else
替代:在遇到需要处理可能为 null
的包装类型时,可以用 if-else
语句替代三目运算符。
ini
代码解读
复制代码
Integer a = null; Integer result; if (a != null) { result = a; } else { result = 0; }
从反编译代码中就能知道这是一种解决方式,
5.总结
在 Java 中使用三目运算符时,要特别注意返回类型的推断是否会导致拆箱操作。若条件结果可能为 null
,而又涉及自动拆箱操作,则会引发空指针异常。因此,确保条件表达式返回值类型一致或明确避免 null
拆箱操作,是解决此问题的关键。