《阿里巴巴Java开发手册》码出高效详解(三)- 磨人的空指针问题解析
1 引导语
话不多说,先看手册指引的规范
手册已经帮助我们总结了常见问题场景,但我们还需要深入研究空指针问题,才能做到发过程中得心应手。
2 问世间空指针为何物
2.1 官方解析
应用需要一个对象时却传入了 null
,包含如下场景:
- 调用 null 对象的实例方法
- 访问或者修改 null 对象的属性
- 获取值为 null 的数组的长度
- 访问或者修改值为 null 的二维数组的列时
- 把 null 当做 Throwable 对象抛出时。
比如说手册中提到的
可归类于 case4
如《手册》中的:
可归类于 case1,因为每一层都可能得到 null。
因此,当在开发中遇到这些场景时,务必注意代码处理,防止空指针。
2.2 继承图
可以清晰地看到 NPE 继承自 RuntimeException ,又属 Exception 的子类。
3. 空指针案例展示
3.1 基本的 bug 生产场景
@Test public void test() { Assertions.assertThrows(NullPointerException.class, () -> { List<UserDTO> users = new ArrayList<>(); users.add(new UserDTO(1L, 3)); users.add(new UserDTO(2L, null)); users.add(new UserDTO(3L, 3)); send(users); }); } // 第 1 处 private void send(List<UserDTO> users) { for (UserDTO userDto : users) { doSend(userDto); } } private static final Integer SOME_TYPE = 2; private void doSend(UserDTO userDTO) { String target = "default"; // 第 2 处 if (!userDTO.getType().equals(SOME_TYPE)) { target = getTarget(userDTO.getType()); } System.out.println(String.format("userNo:%s, 发送到%s成功", userDTO, target)); } private String getTarget(Integer type) { return type + "号基地"; }
- 在第 1 处,如果集合为 null 则抛NPE
- 在第 2 处,如果 type 属性为 null 则抛NPE
大家可能觉得这个例子太简单了,看到输入的参数有 null 自己肯定会考虑空指针问题,但你自己写对外接口时,可并不知道外部传来的参数是否为 null 哦。
3.2 固步自封地返回对象
为了避免空指针或避免检查到 null 参数抛异常,直接返回一个空参构造函数创建的对象:
/** * 根据订单编号查询订单 * * @param orderNo 订单编号 * @return 订单 */ public Order getByOrderNo(String orderNo) { if (StringUtils.isEmpty(orderNo)) { return new Order(); } // 查询order return doGetByOrderNo(orderNo); }
单数据的查询接口,参数检查不符时会抛异常或者返回 null。
少有上述的写法,因为外部调用者的习惯是看到结果不为 null 就直接使用其中的字段。
外部调用判断返回值不为 null , 外部就会大胆得调用实例函数,导致NPE。
3.3 @NonNull 属性反序列化
有一订单更新的 RPC 接口,该接口有一个 OrderUpdateParam 参数,之前有两个属性一个是 id 一个是 name 。
在某个需求时,新增了一个 extra 属性,且该字段不能为 null 。
采用 lombok 的 @NonNull 注解来避免空指针:
import lombok.Data; import lombok.NonNull; import java.io.Serializable; @Data public class OrderUpdateParam implements Serializable { private static final long serialVersionUID = 3240762365557530541L; private Long id; private String name; // 其它属性 // 新增的属性 @NonNull private String extra; }
上线后导致没有使用最新 jar 包的服务对该接口的 RPC 调用报错。
我们来分析一下原因,在 IDEA 的 target - classes 目录下找到 DEMO 编译后的 class 文件,IDEA 会自动帮我们反编译:
public class OrderUpdateParam implements Serializable { private static final long serialVersionUID = 3240762365557530541L; private Long id; private String name; @NonNull private String extra; public OrderUpdateParam(@NonNull final String extra) { if (extra == null) { throw new NullPointerException("extra is marked non-null but is null"); } else { this.extra = extra; } } @NonNull public String getExtra() { return this.extra; } public void setExtra(@NonNull final String extra) { if (extra == null) { throw new NullPointerException("extra is marked non-null but is null"); } else { this.extra = extra; } } // 其他代码 }
还可以使用反编译工具:JD-GUI 对编译后的 class 文件进行反编译,查看源码。
由于调用方调用的是不含 extra 属性的 jar 包,并且序列化编号是一致的,反序列化时会抛出 NPE。
Caused by: java.lang.NullPointerException: extra at com.xxx.OrderUpdateParam.<init>(OrderUpdateParam.java:21)
RPC 参数新增 lombok 的 @NonNull 注解时,要考虑调用方是否及时更新 jar 包,避免出现空指针。
3.4 自动拆箱
案例 1
@Data /** * 我们自己服务的对象 */ public class GoodCreateDTO { private String title; private Long price; private Long count; } @Data /** * 我们调用服务的参数对象 */ public class GoodCreateParam implements Serializable { private static final long serialVersionUID = -560222124628416274L; private String title; private long price; private long count; }
GoodCreateDTO 的 count 字段在我们的系统中是非必传参数,在本系统内可能为 null。
如果我们没有拉取源码的习惯,直接通过前面的转换工具类去转换。我们会潜意识地认为外部接口的对象类型也都是包装类型,这时候很容易因为转换出现 NPE。
- 转换工具类
public class GoodCreateConverter { public static GoodCreateParam convertToParam(GoodCreateDTO goodCreateDTO) { if (goodCreateDTO == null) { return null; } GoodCreateParam goodCreateParam = new GoodCreateParam(); goodCreateParam.setTitle(goodCreateDTO.getTitle()); goodCreateParam.setPrice(goodCreateDTO.getPrice()); goodCreateParam.setCount(goodCreateDTO.getCount()); return goodCreateParam; } }
当转换器执行到 goodCreateParam.setCount(goodCreateDTO.getCount());
会自动拆箱会报NPE
当 GoodCreateDTO 的 count 属性为 null 时,自动拆箱将报NPE
案例 2
调用如下的二方服务接口:
public Boolean someRemoteCall();
然后自以为对方肯定会返回 TRUE 或 FALSE,然后直接拿来作为判断条件或者转为基本类型,如果返回的是 null,则会报NPE。
if (someRemoteCall()) { // 业务代码 }
3.5 分批调用合并结果时空指针
因为某些批量查询的二次接口在数据较大时容易超时,因此可以分为小批次调用。
下面封装一个将 List 数据拆分成每 size 个一批数据,去调用 function RPC 接口,然后将结果合并。
设想如果某个批次请求无数据,不是返回空集合而是 null,会怎样?
很不幸,又一个NPE向你飞来 …
此时要根据具体业务场景来判断如何处理这里可能产生的 NPE
如果在某个场景中,返回值为 null 是一定不允许的行为,可以在 function 函数中对结果进行检查,如果结果为 null,可抛异常。
如果是允许的,在调用 map 后,可以过滤 null :
// 省略前面代码 .map(function) .filter(Objects::nonNull) // 省略后续代码
4 预防指南
4.1 接口开发者
4.1.1 返回空集合
如果参数不符合要求直接返回空集合,底层的函数也使用一致的方式:
public List<Order> getByOrderName(String name) { if (StringUtils.isNotEmpty(name)) { return doGetByOrderName(name); } return Collections.emptyList(); }
4.1.2 使用 Optional
Optional 是 Java 8 引入的特性,返回一个 Optional 则明确告诉使用者结果可能为空:
public Optional<Order> getByOrderId(Long orderId) { return Optional.ofNullable(doGetByOrderId(orderId)); }
如果大家感兴趣可以进入 Optional 的源码,结合前面介绍的 codota 工具进行深入学习,也可以结合《Java 8 实战》的相关章节进行学习。
4.1.3 使用空对象设计模式
该设计模式为了解决 NPE 产生原因的case1,经常需要先判空再继续执行方法:
public void doSomeOperation(Operation operation) { int a = 5; int b = 6; if (operation != null) { operation.execute(a, b); } }
《设计模式之禅》(第二版)554 页在拓展篇讲述了 “空对象模式”。
可以构造一个 NullXXX 类拓展自某个接口, 这样这个接口需要为 null 时,直接返回该对象即可:
public class NullOperation implements Operation { @Override public void execute(int a, int b) { // do nothing } }
这样上面的判空操作就不再有必要, 因为我们在需要出现 null 的地方都统一返回 NullOperation,而且对应的对象方法都是有的:
public void doSomeOperation(Operation operation) { int a = 5; int b = 6; operation.execute(a, b); }
4.2 接口调用者
4.2.1 null 检查
正如 clean code 所说
可以进行参数检查,对不满足的条件抛出异常。
直接在使用前对不能为 null 的和不满足业务要求的条件进行检查,是一种最简单最常见的做法。
通过防御性参数检测,可以极大降低出错的概率,提高程序的健壮性:
@Override public void updateOrder(OrderUpdateParam orderUpdateParam) { checkUpdateParam(orderUpdateParam); doUpdate(orderUpdateParam); } private void checkUpdateParam(OrderUpdateParam orderUpdateParam) { if (orderUpdateParam == null) { throw new IllegalArgumentException("参数不能为空"); } Long id = orderUpdateParam.getId(); String name = orderUpdateParam.getName(); if (id == null) { throw new IllegalArgumentException("id不能为空"); } if (name == null) { throw new IllegalArgumentException("name不能为空"); } }
- JDK线程池方法
- Spring 中的AbstractApplicationContext#assertBeanFactoryActive
4.2.2 使用 Objects
可以使用 Java 7 引入的 Objects 类,来简化判空抛出空指针的代码。
使用方法如下:
private void checkUpdateParam2(OrderUpdateParam orderUpdateParam) { Objects.requireNonNull(orderUpdateParam); Objects.requireNonNull(orderUpdateParam.getId()); Objects.requireNonNull(orderUpdateParam.getName()); }
- 原理很简单,我们看下源码
4.2.3 使用 commons 工具包
4.2.3.1 字符串工具类:org.apache.commons.lang3.StringUtils
public void doSomething(String param) { if (StringUtils.isNotEmpty(param)) { // 使用param参数 } }
4.2.3.2 校验工具类:org.apache.commons.lang3.Validate
public static void doSomething(Object param) { Validate.notNull(param,"param must not null"); } public static void doSomething2(List<String> parms) { Validate.notEmpty(parms); }
该校验工具类支持多种类型的校验,支持自定义提示文本等。
public static <T extends Collection<?>> T notEmpty(final T collection, final String message, final Object... values) { if (collection == null) { throw new NullPointerException(String.format(message, values)); } if (collection.isEmpty()) { throw new IllegalArgumentException(String.format(message, values)); } return collection; }
该如果集合对象为 null 则会抛NPE 如果集合为空则抛出 IllegalArgumentException。
4.2.4 集合工具类:org.apache.commons.collections4.CollectionUtils
public void doSomething(List<String> params) { if (CollectionUtils.isNotEmpty(params)) { // 使用params } }
4.2.5 使用 guava 包
可以使用 guava 包的 com.google.common.base.Preconditions
前置条件检测类。
同样看源码,源码给出了一个范例。原始代码如下:
public static double sqrt(double value) { if (value < 0) { throw new IllegalArgumentException("input is negative: " + value); } // calculate square root }
使用 Preconditions 后,代码可以简化为:
public static double sqrt(double value) { checkArgument(value >= 0, "input is negative: %s", value); // calculate square root }
Spring 例子:
- org.springframework.context.annotation.AnnotationConfigApplicationContext#register
- org.springframework.util.Assert#notEmpty(java.lang.Object[], java.lang.String)
虽然使用的具体工具类不一样,核心的思想都是一致的。
4.2.6 自动化 API
4.2.6.1 lombok # @Nonnull
public void doSomething5(@NonNull String param) { // 使用param proccess(param); }
编译后的代码:
public void doSomething5(@NonNull String param) { if (param == null) { throw new NullPointerException("param is marked non-null but is null"); } else { this.proccess(param); } }
4.2.6.2 IntelliJ IDEA# @NotNull && @Nullable
maven 依赖如下:
<!-- https://mvnrepository.com/artifact/org.jetbrains/annotations --> <dependency> <groupId>org.jetbrains</groupId> <artifactId>annotations</artifactId> <version>17.0.0</version> </dependency>
@NotNull 在参数上的用法和上面的例子非常相似。
public static void doSomething(@NotNull String param) { // 使用param proccess(param); }
5. 总结
本节主要讲述空指针的含义,空指针常见的中枪姿势,以及如何避免空指针异常。下一节将为你揭秘 当 switch 遇到空指针,又会发生什么奇妙的事情。
参考
- 《 阿里巴巴Java 开发手册 1.5.0:华山版》
- 《Java Language Specification: Java SE 8 Edition》
- 码出规范:《阿里巴巴Java开发手册》详解
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
02月06日云栖号头条:阿里巴巴呼吁全球中小企业共同抗疫,开通“防疫直采全球寻源平台”
云栖号:https://yqh.aliyun.com第一手的上云资讯,不同行业精选的上云企业案例库,基于众多成功案例萃取而成的最佳实践,助力您上云决策! 今日最新云头条快讯: 近日,阿里巴巴发信致谢海内外各界,并开通“防疫直采全球寻源平台”,呼吁全球中小企业加入到抗击新型冠状病毒感染的肺炎疫情的行列;习近平2月5日下午主持召开中央全面依法治国委员会第三次会议并发表重要讲话。他强调,要在党中央集中统一领导下,始终把人民群众生命安全和身体健康放在第一位,从立法、执法、司法、守法各环节发力,全面提高依法防控、依法治理能力,为疫情防控工作提供有力法治保障。 一起来看最新的资讯: 阿里巴巴呼吁全球中小企业共同抗疫,开通“防疫直采全球寻源平台” 近日,阿里巴巴发信致谢海内外各界,并开通“防疫直采全球寻源平台”,呼吁全球中小企业加入到抗击新型冠状病毒感染的肺炎疫情的行列。通过这个平台,全球商贸及生产企业上传的医疗物资供应信息,将与平台发布的需求信息进行匹配,最大限度寻找货源、扩大产能;再由阿里巴巴直接采购,将医用口罩等紧缺防疫用品,定点送往疫情防控一线,特别是医院。 中国移动5G医护机器人武汉上岗,...
- 下一篇
Ubuntu安装GDAL 2.1
希望疫情早日得到控制,今天来回顾下之前提到的如何在Linux系统下安装GDAL,本文以Ubuntu为例。 1 GDAL简介 GDAL全称为Geospatial Data Abstraction Library,是当前GIS和遥感领域最为知名和基础的开源库。它实现了基础的栅格与矢量文件的读写以及众多相关的基础空间分析功能,当然矢量文件主要依赖OGR来实现。有非常多的GIS软件都有使用到GDAL/OGR库,包括Esri ArcGIS系列,Google Earth以及开源的GRASS GIS软件。 如果你想在GIS和遥感领域有所建树,那么强烈建议你,走出ArcGIS,多试试不用ArcGIS能做到什么事。ArcGIS很强大,但是并不是离开ArcGIS就没有GIS了。如果只拘泥在ArcGIS上,很有可能被限制住。所以推荐的第一步,就是从安装GDAL开始。 当然GDAL本身在Windows上安装也比较费劲,下次有机会来讲一讲这块。本次主要介绍在Linux——Ubuntu上安装。 2 Ubuntu安装教程 这次主要是在自己的Linux子系统(WSL)上安装。首先其实Linux安装GDAL有一种简便方...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS关闭SELinux安全模块
- CentOS8编译安装MySQL8.0.19
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS7,CentOS8安装Elasticsearch6.8.6
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装