是时候优雅的和NullPointException说再见了

️️️️️️️️️️️️️️️️

最近在参加原创投稿比赛,本篇文章如果对你有帮助的话,欢迎帮忙点击助力下吧


NullPointException应该算是每一个码农都很熟悉的家伙了吧?谁的代码不曾抛过几个空指针异常呢...

比如:你写了段如下的代码:


public void getCompanyFromEmployee() {
Employee employee = getEmployee();
Company company = employee.getTeam().getDepartment().getCompany();
System.out.println(company);
} private Employee getEmployee() {
Employee employee = new Employee();
employee.setEmployeeName("JiaGouWuDao");
employee.setTeam(new Team("DevTeam4"));
return employee;
}

运行程序,你可能就等不到你需要的结果,而是要喜提NullPointException了...

作为JAVA开发中最典型的异常类型,甚至可能是很多程序员入行之后收到的第一份异常大礼包类型。而NullPointException也似乎成为了一种魔咒,迫使程序员在敲出的每一行代码的时候都需要去思考下是否需要去做一下判空操作,久而久之,代码中便充斥着大量的null检查逻辑。

于是呢,上面的代码会变成下面这样:


public void getCompanyFromEmployee() {
Employee employee = getEmployee();
if (employee == null) {
// do something here...
return;
}
Team team = employee.getTeam();
if (team == null) {
// do something here...
return;
}
Department department = team.getDepartment();
if (department == null) {
// do something here...
return;
}
Company company = department.getCompany();
System.out.println(company);
}

是不是大家的项目中都有见过这种写法的?每行代码中都流露着对NullPointException的恐惧有木有?是不是像极了一颗被深深伤害过的心在小心翼翼的保护着自己?

null的困扰

通过上面代码示例,我们可以发现使用null可能会带来的一系列困扰:

  • 空指针异常,导致代码运行时变得不可靠,稍不留神可能就崩了
  • 使代码膨胀,导致代码中充斥大量的null检查与保护,使代码可读性降低

此外,null还有一个明显的弊端:

  • 含义不明确,比如一个方法返回了null,调用方不清楚到底是因为逻辑有问题导致为null,还是说null其实也是一种可以接受的正常返回值类型?

所以说,一个比较好的编码习惯,是尽量避免在程序中使用null,可以按照具体的场景分开区别对待:

  • 确定是因为代码或者逻辑层面处理错误导致的无值,通过throw异常的方式,强制调用方感知并进行处理对待
  • 如果null代表业务上的一种正常可选值,可以考虑返回Optional来替代。

当然咯,有时候即使我们自己的代码不返回null,也难免会遇到调用别人的接口返回null的情况,这种时候我们真的就只能不停的去判空来保护自己吗?有没有更优雅的应对策略来避免自己掉坑呢?下面呢,我们一起探讨下null的一些优雅应对策略。

Optional应对null处理

Optional一定比return null安全吗

前面我们提到了说使用Optional来替代null,减少调用端的判空操作压力,防止调用端出现空指针异常。

那么,使用返回Optional对象就一定会比return null更靠谱吗?

答案是:也不一定,关键要看怎么用!

比如:下面的代码,getContent()方法返回了个Optional对象,然后testCallOptional()方法作为调用方,获取到返回值后的操作方式:


public void testCallOptional() {
Optional<Content> optional = getContent();
System.out.println("-------下面代码会报异常--------");
try {
// 【错误用法】直接从Optional对象中get()实际参数,这种效果与返回null对象然后直接调用是一样的效果
Content content = optional.get();
System.out.println(content);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("-------上面代码会报异常--------");
} private Optional<Content> getContent() {
return Optional.ofNullable(null);
}

上述代码运行之后会发现报错了:

-------下面代码会报异常--------
java.util.NoSuchElementException: No value present
at java.util.Optional.get(Optional.java:135)
at com.veezean.skills.optional.OptionalService.testCallOptional(OptionalService.java:47)
at com.veezean.skills.optional.OptionalService.main(OptionalService.java:58)
-------上面代码会报异常--------

既然直接调用Optional.get()报错,那就是调用前加个判断就好咯?


public void testCallOptional2() {
Optional<Content> optional = getContent();
// 使用前先判断下元素是否存在
if (optional.isPresent()) {
Content content = optional.get();
System.out.println(content);
}
}

执行一下,果然不报错了。但是,这样真的就是解决方法吗?这样跟直接返回null然后使用前判空(下面的写法)其实也没啥区别,也并不会让调用方使用起来更加的优雅与靠谱:


public void testNullReturn2() {
Content content = getContent2();
if (content != null) {
System.out.println(content.getValue());
}
}

那怎么样才是正确的使用方式呢,下面一起来看下。

全面认识下Optional

创建Optional对象

Optional<T>对象,可以用来表示一个T类型对象的封装,或者也可以表示不是任何对象。Optional类提供了几个静态方法供对象的构建:

方法名 功能含义描述
empty() 构造一个无任何实际对象值的空Optional对象(可以理解为业务层面的null
of(T t) 根据给定的对象,构造一个此对象的封装Optional对象,注意入参t不能为null,否则会空指针
ofNullable(T t) 根据传入的入参t的值构造Optional封装对象,如果传入的t为null,则等同于调用empty()方法,如果t不为null,则等同于调用of(T t)方法

在项目中,我们可以选择使用上面的方法,实现Optional对象的封装:


public void testCreateOptional() {
// 使用Optional.of构造出具体对象的封装Optional对象
System.out.println(Optional.of(new Content("111","JiaGouWuDao")));
// 使用Optional.empty构造一个不代表任何对象的空Optional值
System.out.println(Optional.empty());
System.out.println(Optional.ofNullable(null));
System.out.println(Optional.ofNullable(new Content("222","JiaGouWuDao22")));
}

输出结果:


Optional[Content{id='111', value='JiaGouWuDao'}]
Optional.empty
Optional.empty
Optional[Content{id='222', value='JiaGouWuDao22'}]

这里需要注意下of方法如果传入null会抛空指针异常,所以比较建议大家使用ofNullable方法,可以省去调用前的额外判空操作,也可以避免无意中触发空指针问题:

Optional常用方法理解

在具体讨论应该如何正确使用Optional的方法前,先来了解下Optional提供的一些方法:

方法名 含义说明
isPresent 如果Optional实际有具体对象值,则返回true,否则返回false。
ifPresent 这是一个函数式编程风格的API接口,入参是一个函数,即如果Optional对象有实际对象值,则会执行传入的入参函数逻辑,如果不存在实际对象值,则不会执行传入的入参函数逻辑。
get 返回Optional封装的实际对象T数据,注意,如果实际对象数据不存在,会抛异常而非返回null
orElse get方法类似,都是获取Optional实际的对象值,区别在于orElse必须传入一个默认值,当Optional没有实际值的时候返回默认值而非抛异常
orElseGet 可以理解为orElse方法的升级版,区别在于orElse仅允许传入一个固定的默认值,而orElseGet的入参是一个函数方法,当Optional无实际值时,会执行给定的入参函数,返回动态值
orElseThrow orElse类似,区别在于如果没有获取到,会抛出一个指定的异常
filter 判定当前Optional的实际对象是否符合入参函数的过滤规则,如果符合则返回当前Optional对象,如果不符合则返回空Optional
map 接收一个入参函数,允许将Optional中的实际对象值处理转换为另一实际对象值(这个入参函数的返回值为T),并生成返回此新类型的Optional对象,如果生成的新对象为null,则返回一个空Optional对象
flatMap map类似,区别点在于入参函数的返回值类型有区别(此处入参函数的返回值为Optional<T>

看到这里的mapflatMap方法,不知道大家会不会联想到Stream流对象操作的时候也有这两个方法的身影呢(不了解的同学可以戳这个链接抓紧补补课:吃透JAVA的Stream流操作)?的确,它们的作用也是类似的,都是用来将一个对象处理转换为另一个对象类型的:

对于Optional而言,mapflatMap最终的实现效果其实都是一样的,仅仅只是入参的要求不一样,也即两种不同写法,两者区别点可以通过下图来理解:

实际使用的时候,可以根据需要选择使用map或者flatMap


public void testMapAndFlatMap() {
Optional<User> userOptional = getUser();
Optional<Employee> employeeOptional = userOptional.map(user -> {
Employee employee = new Employee();
employee.setEmployeeName(user.getUserName());
// map与flatMap的区别点:此处return的是具体对象类型
return employee;
});
System.out.println(employeeOptional); Optional<Employee> employeeOptional2 = userOptional.flatMap(user -> {
Employee employee = new Employee();
employee.setEmployeeName(user.getUserName());
// map与flatMap的区别点:此处return的是具体对象的Optional封装类型
return Optional.of(employee);
});
System.out.println(employeeOptional2);
}

从输出结果可以看出,两种不同的写法,实现是相同的效果:


Optional[Employee(employeeName=JiaGouWuDao)]
Optional[Employee(employeeName=JiaGouWuDao)]

Optional使用场景

减少繁琐的判空操作

再回到本篇文章最开始的那段代码例子,如果我们代码里面不去逐个做判空保护的话,我们可以如何来实现呢?看下面的实现思路:


public void getCompanyFromEmployeeTest() {
Employee employeeDetail = getEmployee();
String companyName = Optional.ofNullable(employeeDetail)
.map(employee -> employee.getTeam())
.map(team -> team.getDepartment())
.map(department -> department.getCompany())
.map(company -> company.getCompanyName())
.orElse("No Company");
System.out.println(companyName);
}

先通过map的方式一层一层的去进行类型转换,最后使用orElse去获取Optional中最终处理后的值,并给定了数据缺失场景的默认值。是不是看着比一堆if判空操作要舒服多了?

适用场景

需要通过某个比较长的调用链路一层一层去调用获取某个值的时候,使用上述方法,可以避免空指针以及减少冗长的判断逻辑。

需要有值兜底的数据获取场景

编码的时候,经常会遇到一些数据获取的场景,需要先通过一些处理逻辑尝试获取一个数据,如果没有获取到需要的数据,还需要返回一个默认值,或者是执行另一处理逻辑继续尝试获取

比如从请求头中获取客户端IP的逻辑,按照常规逻辑,代码会写成下面这样:


public String getClientIp(HttpServletRequest request) {
String clientIp = request.getHeader("X-Forwarded-For");
if (!StringUtils.isEmpty(clientIp)) {
return clientIp;
}
clientIp = request.getHeader("X-Real-IP");
return clientIp;
}

但是借助Optional来实现,可以这样写:


public String getClientIp2(HttpServletRequest request) {
String clientIp = request.getHeader("X-Forwarded-For");
return Optional.ofNullable(clientIp).orElseGet(() -> request.getHeader("X-Real-IP"));
}

适用场景

优先执行某个操作尝试获取数据,如果没获取到则去执行另一逻辑获取,或者返回默认值的场景。

替代可能为null的方法返回值

下面是一段从项目代码中截取的片段:


public FileInfo queryOssFileInfo(String fileId) {
FileEntity entity = fileRepository.findByIdAndStatus(fileId, 0);
if (entity != null) {
return new FileInfo(entity.getName(), entity.getFilePath(), false);
}
FileHistoryEntity hisEntity = fileHisRepository.findByIdAndStatus(fileId, 0);
if (hisEntity != null) {
return new FileInfo(hisEntity.getName(), hisEntity.getFilePath(), true);
}
return null;
}

可以看到最终的return分支中,有一种可能会返回null,这个方法作为项目中被高频调用的一个方法,意味着所有的调用端都必须要做判空保护。可以使用Optional进行优化处理:


public Optional<FileInfo> queryOssFileInfo(String fileId) {
FileEntity entity = fileRepository.findByIdAndStatus(fileId, 0);
if (entity != null) {
return Optional.ofNullable(new FileInfo(entity.getName(), entity.getFilePath(), false));
}
FileHistoryEntity hisEntity = fileHisRepository.findByIdAndStatus(fileId, 0);
if (hisEntity != null) {
return Optional.ofNullable(new FileInfo(hisEntity.getName(), hisEntity.getFilePath(), true));
}
return Optional.empty();
}

这样的话,就可以有效的防止调用端踩雷啦~

适用场景

实现某个方法的时候,如果方法的返回值可能会为null,则考虑将方法的返回值改为Optional类型,原先返回null的场景,使用Optional.empty()替代。

包装数据实体中非必须字段

首先明确一下,Optional的意思是可选的,也即用于标识下某个属性可有可无的特性。啥叫可有可无?看下面代码:


public class PostDetail {
private String title;
private User postUser;
private String content;
private Optional<Date> lastModifyTime = Optional.empty();
private Optional<Attachment> attachment = Optional.empty();
}

上面是一个帖子详情数据类,对于一个论坛帖子数据而言,帖子的标题、内容、发帖人这些都是属于必须的字段,而帖子的修改时间、帖子的附件其实是属于可选字段(因为不是所有的帖子都会被修改、也不是所有帖子都会带附件),所以针对这种可有可无的字段,就可以声明定义的时候使用Optional进行封装。

使用Optional进行封装之后有两个明显的优势:

  • 强烈的业务属性说明,明确的让人知晓这个是一个可选字段,等同于数据库建表语句里面设置nullable标识一样的效果;
  • 调用端使用的时候也省去了判空操作。

适用场景

数据实体定义的时候,对于可选参数,采用Optional封装类型替代。

使用抛异常替代return null

相比于返回一个Optional封装的对象,直接抛异常具有强烈的警醒意味,意在表达此处存在预期之外的不合理情况,要求编码的时候,调用端必须要予以专门处理


public Team getTeamInfo() throws TestException {
Employee employee = getEmployee();
Team team = employee.getTeam();
if (team == null) {
throw new TestException("team is missing");
}
return team;
}

相比直接return null,显然抛异常的含义更加明确。

JDK与开源框架的实践

JDK提供的很多方法里面,其实都是遵循着本文中描述的这种返回值处理思路的,很少会看到直接返回null的——不止JDK,很多大型的开源框架源码中,也很少会看到直接return null的情况。

比如com.sun.jmx.snmp.agent中的一段代码:


public SnmpMibSubRequest nextElement() throws NoSuchElementException {
if (iter == 0) {
if (handler.sublist != null) {
iter++;
return hlist.getSubRequest(handler);
}
}
iter ++;
if (iter > size) throw new NoSuchElementException();
SnmpMibSubRequest result = hlist.getSubRequest(handler,entry);
entry++;
return result;
}

再比如Spring中org.springframework.data.jpa.repository.support包下面的方法例子:


public Optional<T> findById(ID id) {
Assert.notNull(id, ID_MUST_NOT_BE_NULL);
Class<T> domainType = getDomainClass();
if (metadata == null) {
return Optional.ofNullable(em.find(domainType, id));
}
LockModeType type = metadata.getLockModeType();
Map<String, Object> hints = getQueryHints().withFetchGraphs(em).asMap();
return Optional.ofNullable(type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints));
}

总结

好啦,关于编码中对null的一些应对处理策略与思路呢,这里就给大家分享到这里,希望可以对大家有所启发,通过不断的细节优化与改进,最终摆脱被空指针摆布的局面~

那么,对上面提到的一些内容与场景,你是否也有遇到相关的情况呢?你是怎么处理的呢?欢迎多切磋交流下~

此外:

我是悟道,聊技术、又不仅仅聊技术~

如果觉得有用,请点个关注,也可以关注下我的公众号【架构悟道】,获取更及时的更新。

期待与你一起探讨,一起成长为更好的自己。

是时候优雅的和NullPointException说再见了的更多相关文章

  1. 使用 Java8 Optional 的正确姿势(转)

    我们知道 Java 8 增加了一些很有用的 API, 其中一个就是 Optional. 如果对它不稍假探索, 只是轻描淡写的认为它可以优雅的解决 NullPointException 的问题, 于是代 ...

  2. [JDK8] Optional

    我们知道 Java 8 增加了一些很有用的 API, 其中一个就是 Optional. 如果对它不稍假探索, 只是轻描淡写的认为它可以优雅的解决 NullPointException 的问题, 于是代 ...

  3. [转] 使用 Java8 Optional 的正确姿势

    [From] https://unmi.cc/proper-ways-of-using-java8-optional/ 我们知道 Java 8 增加了一些很有用的 API, 其中一个就是 Option ...

  4. 如何优雅的调戏XSS

    作者:i春秋作家——万年死宅 前言 这篇paper,我们将学习如何优雅的调戏XSS.我们会教大家一些不常用的,但很实用的XSS姿势.我们在正式进入主题之前,先来说一下,该篇paper将涉及的内容: 正 ...

  5. Java 8 Optional:优雅地避免 NPE

    本篇文章将详细介绍 Optional 类,以及如何用它消除代码中的 null 检查.在开始之前首先来看下什么是 NPE,以及在 Java 8 之前是如何处理 NPE 问题的. 空指针异常(NullPo ...

  6. java秀发入门到优雅秃头路线导航【教学视频+博客+书籍整理】

    目录 一.Java基础 二.关于JavaWeb基础 三.关于数据库 四.关于ssm框架 五.关于数据结构与算法 六.关于开发工具idea 七.关于项目管理工具Mawen.Git.SVN.Gradle. ...

  7. 如何写出优雅的Python代码?

    有时候你会看到很Cool的Python代码,你惊讶于它的简洁,它的优雅,你不由自主地赞叹:竟然还能这样写.其实,这些优雅的代码都要归功于Python的特性,只要你能掌握这些Pythonic的技巧,你一 ...

  8. Guava - 拯救垃圾代码,写出优雅高效,效率提升N倍

    最近在看一个同学代码的时候,发现代码中大量使用了 Google 开源的 Guava 核心库中的内容,让代码简单清晰了不少,故学习分享出 Guava 中我认为最实用的功能. Guava 项目是 Goog ...

  9. [Egret]优雅的写http

    首先,自从使用链式调用的写法后,就一发不可收拾的喜爱上了这种优雅的方式.不管是写架构还是写模块,我都会不自觉的使用这种最优雅的方式.链式写法既减少了代码量,又非常优雅的. 在使用 egret 的htt ...

随机推荐

  1. python中的sort用法

    内置的列表类型提供sort的方法 可以根据多项指标给list实例中的元素排序.在默认情况下,sort方法总是按照自然升序排列列表内的元素 #升序排列 list1=[2,3,1,2,5] list1.s ...

  2. MySQL性能优化 - 别再只会说加索引了

    MySQL性能优化 MySQL性能优化我们可以从以下四个维度考虑:硬件升级.系统配置.表结构设计.SQL语句和索引. 从成本上来说:硬件升级>系统配置>表结构设计>SQL语句及索引, ...

  3. 小程序扫码、上传图片、css时间轴

    de <!-- 导航 --> <view class="navSec flexBox"> <text class="navItem {{ s ...

  4. Blazor和Vue对比学习(基础1.4):事件和子传父

    Blazor和Vue的组件事件,都使用事件订阅者模式.相对于上一章的组件属性,需要多绕一个弯,无论Blazor还是Vue,都是入门的第一个难点.要突破这个难点,一是要熟悉事件订阅模式<其实不难& ...

  5. .NET混合开发解决方案16 管理WebView2的用户数据

    系列目录     [已更新最新开发文章,点击查看详细] WebView2控件应用详解系列博客 .NET桌面程序集成Web网页开发的十种解决方案 .NET混合开发解决方案1 WebView2简介 .NE ...

  6. 强制20天加班开发app后被集体解雇,象寻技术负责人公众号发文怒斥前领导

    5月16日下午三点,象寻官方公众号发了一篇<祝象寻早日倒闭的文章>文章,文章配一个竖中指的手势.如此劲爆的文章瞬间引爆了微信朋友圈,大家纷纷分享给好友和微信群,阅读量也达到了十万+. 当时 ...

  7. 101_Power Pivot DAX 累计至今,历史累计至今

    焦棚子的文章目录 一.背景 DAX中已经有诸如YTD,QTD,MTD时间智能函数.用起来也比较方便. 但很多时候需要看历史累计至今的数据,需要自己根据实际情况写dax. 今天抛砖引玉,写一个示例. 二 ...

  8. 关于我学git这档子事

    创建本地分支并切换到该分支 git checkout -b *** 相当于如下2个命令: git branch *** git checkout *** 推送本地开发分支到远程开发分支 git pus ...

  9. Solon 1.8.0 发布,云原生微服务开发框架

    相对于 Spring Boot 和 Spring Cloud 的项目 启动快 5 - 10 倍 qps 高 2- 3 倍 运行时内存节省 1/3 ~ 1/2 打包可以缩小到 1/2 ~ 1/10(比如 ...

  10. vivo 容器集群监控系统架构与实践

    vivo 互联网服务器团队-YuanPeng 一.概述 从容器技术的推广以及 Kubernetes成为容器调度管理领域的事实标准开始,云原生的理念和技术架构体系逐渐在生产环境中得到了越来越广泛的应用实 ...