还在重复写空指针检查代码?考虑使用 Optional 吧!

一、前言
如果要给 Java 所有异常弄个榜单,我会选择将 NullPointerException 放在榜首。这个异常潜伏在代码中,就像个遥控炸弹,不知道什么时候这个按钮会被突然按下(传入 null 对象)。
还记得刚入行程序员的时候,三天两头碰到空指针异常引发的 Bug,解决完一个,又在另一处碰到。那时候师兄就教我,不要相信任何『对象』,特别是别人给你的,这些地方都加上判断。于是代码通常为会变成下面这样:
if(obj!=null){
// do something
}
有了这个防御之后,虽然不用再担心空指针异常,但是过多的判断语句使得代码变得臃肿。
假设我们存在如下对象关系

原本为了获取图中的 name 属性,原本一句代码就可以轻松完成。
Staff staff=..;
staff.getDepartment().getCompany().getName();
但是很不幸,为了代码的安全性,我们不得不加入空指针判断代码。
Staff staff=..;
if (staff != null) {
Department department = staff.getDepartment();
if (department != null) {
Company company = department.getCompany();
if (company != null) {
return company.getName();
}
}
}
return "Unknown";
当其中对象为
null时,可以返回默认值,如上所示。也可以直接抛出其他异常快速失败。
虽然上面代码变得更加安全,但是过多嵌套 if 语句降低代码整体可读性,提高复杂度。
所幸 Java 8 引入引入一个新类 Java.util.Optional<T>,依靠 Optional 类提供 API,我们可以写出既安全又具有阅读性的代码。
还在使用 JDK 6 ?那你也别急着关闭这篇文章。可以考虑使用 Guava Optional。不过需要注意的是,Guava Optional API 与 JDK 存在差异,以下以 JDK8 Optional 为例。
二、Optional API
2.1、Optional#of 与 Optional#ofNullable
Optional 本质是一个容器,需要我们将对象实例传入该容器中。Optional 的构造方法为 private,无法直接使用 new 构建对象,只能使用 Optional 提供的静态方法创建。
Optional 三个创建方法如下:
Optional.of(obj),如果对象为 null,将会抛出 NPE。Optional.ofNullable(obj),如果对象为 null,将会创建不包含值的 emptyOptional对象实例。Optional.empty()等同于Optional.ofNullable(null)

只有在确定对象不会为 null 的情况使用 Optional#of,否则建议使用 Optional#ofNullable方法。
2.2、Optional#get 与 Optional#isPresent
对象实例存入 Optional 容器中之后,最后我们需要从中取出。Optional#get 方法用于取出内部对象实例,不过需要注意的是,如果是 empty Optional 实例,由于容器内没有任何对象实例,使用 get 方法将会抛出 NoSuchElementException 异常。
为了防止异常抛出,可以使用 Optional#isPresent 。这个方法将会判断内部是否存在对象实例,若存在则返回 true。
示例代码如下:
Optional<Company> optCompany = Optional.ofNullable(company);
// 与直接使用空指针判断没有任何区别
if (optCompany.isPresent()) {
System.out.println(optCompany.get().getName());
}
仔细对比,可以发现上面用法与空指针检查并无差别。刚接触到 Optional ,看到很多文章介绍这个用法,那时候一直很疑惑,这个解决方案不是更加麻烦?
后来接触到 Optional 其他 API,我才发现这个类真正有意义是下面这些 API。如果使用过 Java8 Stream 的 API,下面 Optional API 你将会很熟悉。
2.3、Optional#ifPresent
通常情况下,空指针检查之后,如果对象不为空,将会进行下一步处理,比如打印该对象。
Company company = ...;
if(company!=null){
System.out.println(company);
}
上面代码我们可以使用 Optional#ifPresent 代替,如下所示:
Optional<Company> optCompany = ...;
optCompany.ifPresent(System.out::println);
使用 ifPresent 方法,我们不用再显示的进行检查,如果 Optional 为空,上面例子将不再输出。
2.4、Optional#filter
有时候我们需要某些属性满足一定条件,才进行下一步动作。这里假设我们当 Company name 属性为 Apple,打印输出 ok。
if (company != null && "Apple".equals(company.getName())) {
System.out.println("ok");
}
下面使用 Optional#filter 结合 Optional#ifPresent 重写上面的代码,如下所示:
Optional<Company> companyOpt=...;
companyOpt
.filter(company -> "Apple".equals(company.getName()))
.ifPresent(company -> System.out.println("ok"));
filter 方法将会判断对象是否符合条件。如果不符合条件,将会返回一个空的 Optional 。
2.5、Optional#orElse 与 Optional#orElseThrow
当一个对象为 null 时,业务上通常可以设置一个默认值,从而使流程继续下去。
String name = company != null ? company.getName() : "Unknown";
或者抛出一个内部异常,记录失败原因,快速失败。
if (company.getName() == null) {
throw new RuntimeException();
}
Optional 类提供两个方法 orElse 与 orElseThrow ,可以方便完成上面转化。
// 设置默认值
String name=companyOpt.orElse(new Company("Unknown")).getName();
// 抛出异常
String name=companyOpt.orElseThrow(RuntimeException::new).getName();
如果 Optional 为空,提供默认值或抛出异常。
2.6、Optional#map 与 Optional#flatMap
熟悉 Java8 Stream 同学的应该了解,Stream#map 方法可以将当前对象转化为另外一个对象, Optional#map 方法也与之类似。
Optional<Company> optCompany = ...;
Optional<String> nameopt = optCompany.map(Company::getName);
map 方法可以将原先 Optional<Company> 转变成 Optional<String> ,此时 Optional 内部对象变成 String 类型。如果转化之前 Optional 对象为空,则什么也不会发生。
另外 Optional 还有一个 flatMap 方法,两者区别见下图。

Department#getCompany返回对象为Optional<Company>
三、代码重构
上面我们学习了 Optional 类主要 API ,下面我们使用 Optional 重构文章刚开头的代码。为了方便读者对比,将上面的代码复制了下来。
代码重构前:
if (staff != null) {
Department department = staff.getDepartment();
if (department != null) {
Company company = department.getCompany();
if (company != null) {
return company.getName();
}
}
}
return "Unknown";
首先我们需要将 Staff ,Department 修改 getter 方法返回结果类型改成 Optional。
public class Staff {
private Department department;
public Optional<Department> getDepartment() {
return Optional.ofNullable(department);
}
...
}
public class Department {
private Company company;
public Optional<Company> getCompany() {
return Optional.ofNullable(company);
}
...
}
public class Company {
private String name;
public String getName() {
return name;
}
...
}
然后综合使用 Optional API 重构代码如下:
Optional<Staff> staffOpt =...;
staffOpt
.flatMap(Staff::getDepartment)
.flatMap(Department::getCompany)
.map(Company::getName)
.orElse("Unknown");
可以看到重构之后代码利用 Optional 的 Fluent Interface,以及 lambda 表达式,使代码更加流畅连贯,并且提高代码整体可读性。
四、帮助文章
1、Tired of Null Pointer Exceptions? Consider Using Java SE 8's Optional!
3、Optionals: Patterns and Good Practices
3、Java8 in Action
欢迎关注我的公众号:程序通事,获得日常干货推送。如果您对我的专题内容感兴趣,也可以关注我的博客:studyidea.cn

还在重复写空指针检查代码?考虑使用 Optional 吧!的更多相关文章
- ESLint 检查代码质量
利用 ESLint 检查代码质量 其实很早的时候就想尝试 ESLint 了,但是很多次都是玩了一下就觉得这东西巨复杂,一执行检查就是满屏的error,简直是不堪入目,遂放弃.直到某天终于下定决心深入看 ...
- 利用ESLint检查代码质量
1. ESLint ESLint 是一个插件化的 javascript 代码检测工具,它可以用于检查常见的 JavaScript 代码错误,也可以进行代码风格检查,这样我们就可以根据自己的喜好指定一套 ...
- 在IDEA中停止和关闭SonarLint自动检查,手动运行SonarLint检查代码
关闭SonarLint自动检查代码 有时敲一行代码SonarLint插件就会自动检查,让人感觉很不舒服,还会使电脑卡顿: 依次点击:File -> Settings 或直接Ctrl+Alt+S ...
- 使用eslint检查代码质量
1.安装 全局安装 npm install eslint -g 局部安装 npm install eslint --save 2.初始化一个配置文件 eslint --init 执行后根据项目需要回答 ...
- 项目git commit时卡主不良代码:husky让Git检查代码规范化工作
看完 <前端规范之Git工作流规范(Husky + Commitlint + Lint-staged) https://www.cnblogs.com/Yellow-ice/p/15349873 ...
- C#编译器怎么检查代码是否会执行
博客搬到了fresky.github.io - Dawei XU,请各位看官挪步.最新的一篇是:C#编译器怎么检查代码是否会执行.
- 解决python logging重复写日志问题
import logging from homework.exam_homework_0413.common import contants from homework.exam_homework_0 ...
- android 中的一些资源注解,让编译器帮你检查代码
android 中的一些资源注解,让编译器帮你检查代码 写方便的时候可以用注解来声明一些参数,以明确的指示参数的类型,让代码更安全.我们看到,在android源代码里大量使用了注解.我整理了一些注解如 ...
- uboot搬移部分和重定位部分的代码分析
来看一下搬移部分和重定位部分的代码: relocate: /* 把U-BOOT重新定位到RAM*/ //r0=0; adr r0, _start /* r0是代码的当前位置*/ ld ...
随机推荐
- Disruptor原理探讨
之前谈到了在我的项目里用到了Disruptor,因为对它了解不足的原因,才会引发之前的问题,因此,今天特意来探讨其原理. 为什么采用Disruptor 先介绍一下我的这个服务.这个服务主要是作为游戏服 ...
- Cabloy全栈JS框架微创新之一:不一样的“移动优先 PC适配”
前言 目前流行的前端UI组件库都支持移动设备优先的响应式布局特性.但基于Mobile和PC两个场景的不同用户体验,也往往会实现Mobile和PC两个版本. PC场景下的Web工程,如大量的后台前端管理 ...
- 同步FIFO design and IP level verification
一.前言 应聘IC前端相关岗位时,FIFO是最常考也是最基本的题目.FIFO经常用于数据缓存.位宽转换.异步时钟域处理.随着芯片规模的快速增长,灵活的system verilog成为设计/验证人员的基 ...
- Python 踩坑之旅文件系统篇其一文件夹也是个文件
目录 1.1 案例 1.2 分析 1.3 扩展 1.4 技术关键字 下期预告 代码示例支持 平台: Mac OS Python: 2.7.10 代码示例: - wx: 菜单 - Python踩坑指南代 ...
- 面试官: 聊一聊Babel
点击关注本公众号获取文档最新更新,并可以领取配套于本指南的 <前端面试手册> 以及最标准的简历模板. 前言 Babel 是现代 JavaScript 语法转换器,几乎在任何现代前端项目中都 ...
- centos C++ ccache llvm编译环境配置
下载ccache rpm包wget https://centos.pkgs.org/6/epel-x86_64/ccache-3.1.6-2.el6.x86_64.rpm.htmlyum -y ins ...
- 【linux】【jenkins】自动化运维六 构建生成备份
push tag用于提交代码构建成功后push tag,以防提交代码报错,方便回滚之前正常的代码. 由于采用docker部署的形式,构建失败自动回滚还未实现,待研究解决. 构建后操作选择 Git Pu ...
- redis实现网关限流(限制API调用次数1000次/分)
添加maven依赖,使用springboot2.x版本 <dependency> <groupId>org.springframework.boot</groupId&g ...
- jenkins上下游工程以及空间占用处理
1.最近项目架构调整,把十几个java项目整合为一个大的项目,这样构建上游工程成功后下游工程会自动构建 解决如下:取消这个勾选即可 2.构建单个项目时,会把所有子工程都打包一次 解决如下:指定构建时的 ...
- JDBC的批处理学习rewriteBatchedStatements=true
如果在不添加批处理指令的情况下,mysql默认是不使用批处理操作,如果在url尾部添加rewriteBatchedStatements=true 可以使当前连接 使用批处理操作 创建数据库表结构 cr ...