李锋镝的博客

  • 首页
  • 时间轴
  • 说说
  • 左邻右舍
  • 博友圈
  • 关于我
    • 关于我
    • 另一个网站
    • 我的导航站
    • 网站地图
    • 赞助
  • 留言
  • 走心评论
  • 系列文章
  • Now
  • 每日心情
  • 🚇开往
Destiny
自是人生长恨水长东
  1. 首页
  2. 转载
  3. 技术
  4. 正文

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

2022年7月15日 约 3,807 字13 分钟 21点热度 0人点赞 0条评论
本文最后更新于 2022年8月2日,距今已 1466 天,其中的信息可能已经发生变化,请注意甄别。

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了...

是时候优雅的和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的恐惧有木有?是不是像极了一颗被深深伤害过的心在小心翼翼的保护着自己?

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

null的困扰

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

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

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

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

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

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

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

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

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

 

Optional应对null处理

Optional一定比return null安全吗

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

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

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

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


public void testCallOptional() {
    Optional 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 OptionalgetContent() {
    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 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类型对象的封装,或者也可以表示不是任何对象。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方法,可以省去调用前的额外判空操作,也可以避免无意中触发空指针问题:

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

 

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)

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

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

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

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

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


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

    Optional 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 OptionalqueryOssFileInfo(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 lastModifyTime = Optional.empty();
    private Optional attachment = Optional.empty();
}

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

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

使用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 OptionalfindById(ID id) {
    Assert.notNull(id, ID_MUST_NOT_BE_NULL);
    Class domainType = getDomainClass();
    if (metadata == null) {
        return Optional.ofNullable(em.find(domainType, id));
    }
    LockModeType type = metadata.getLockModeType();
    Map hints = getQueryHints().withFetchGraphs(em).asMap();
    return Optional.ofNullable(type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints));
}

总结

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

 

原文链接:https://www.cnblogs.com/softwarearch/p/16478112.html

除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接

本文链接:https://www.lifengdi.com/transport/technology/3910

推荐阅读

  • ThreadLocal如何解决内存泄漏问题
  • Redisson分布式锁的watch dog自动续期机制
  • SpringBoot整合MongoDB
  • RocketMQ入门级教程
  • @Async注解的坑
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: JAVA
最后更新:2022年8月2日

岁月同一天 8 月 8 日

回望过去的今天,你在写什么

  • 4 年前 2022年8月8日
    减肥四个月~

    今年四月初开始每天跳绳减肥,到了今天刚好四个月了。从一开始的每天跳绳十分钟到半个小时,再从半个小时到一个小时,最后又稳定…

相关文章
  • SpringBoot整合MongoDB2022年9月2日
  • jmap命令(jdk1.8)2020年9月27日
  • 解决Cannot connect to core dump or remote debug server. Use jhsdb jmap instead2025年4月9日
  • Java整数运算2022年7月15日
  • 【收藏】Java问题排查相关工具工具箱2019年10月16日

李锋镝

既然选择了远方,便只顾风雨兼程。

打赏 点赞
< 上一篇
下一篇 >
1234567891112131415161718192021222324252627282930313233343536373839404142434446474849505152535455575859606162636465666769727476777879808182858687909293949596979899
取消回复
…

文章评论

还没有评论,快来抢沙发吧~

九年没联系的女友突然问我借钱,我开口道:四十万行么?不够了再说话。”前女友很是高兴地回复说:“够用,还是我男朋友有办法啊,你放心,我日后一定还给你,九年多没见了你现在干啥呢,这么有钱?”我呵呵一笑:“没事,不够用您尽管说话,俺现在改做抵押贷款了。”

听点儿音乐吧 朋友~
文章目录
最新 热点 随机
最新 热点 随机
Kratos+ v1.1.14版本更新说明 Spring Boot 指定外部配置文件的方式 Spring Boot 配置加载优先级总结 Claude Fable 5(claude-fable-5)深度详解 如何通过命令查看Java应用内存中对象数量 关于服务的探活端口和业务端口不一致有什么问题
给主题增加了Now、每日心情、年度回顾、岁月同一天、随机漫步等功能AI时代,个人技术博客的出路在哪里?增加了两套复古皮肤-牛皮纸、千禧网页这个域名注册整整十年了,十年时间,真快啊WordPress实现用户评论等级排行榜插件WordPress网站换了个字体,差点儿把样式换崩了
MybatisCodeHelperPro激活 解决kubectl exec -it xxxx-service-bfbd45bb9-ktvzj bash -n bit error: exec [POD] [COMMAND] is not supported anymore. Use exec [POD] -- [COMMAND] instead See 'kubectl exec -h' for help and examples Apollo配置中心中的protalDB的作用是什么 k8s + docker + Jenkins使用Pipeline部署SpringBoot项目时Jenkins错误集锦 彻底搞懂mysql日志系统binlog,redolog,undolog Spring事件驱动深度指南:从单机异步到亿级流量,比MQ更轻的架构神器
最近评论
Huo 发布于 8 小时前(08月07日) 挺有特色的主题,还是感觉 WP 的确是强大
李锋镝 发布于 21 小时前(08月07日) 没理解你想说啥
aboss 发布于 21 小时前(08月07日) 你的后台web-login?
李锋镝 发布于 22 小时前(08月07日) 这个专门的插件实现的功能更好更全,还能对接支付之类的
李锋镝 发布于 22 小时前(08月07日) 自定义登录地址是为了防止大部分机器人通过WP固定登录页面暴力破解用户账号密码
标签聚合
K8s 日常 Claude docker JVM WordPress Redis AI编程 多线程 SQL 架构 MySQL IDEA JAVA SpringBoot Spring AI 数据库 ElasticSearch 分布式
友情链接
  • Blogs·CN
  • 懋和道人
  • 旧时繁华
  • Honesty
  • 拾趣博客导航
  • 搬砖日记
  • 知向前端
  • 临窗旋墨
  • 皮皮社
  • 瓦匠个人小站
  • 志文工作室
  • 韩小韩博客
  • 风渡言
  • 彬红茶日记
  • 老张博客
  • Mr.Sun的博客
  • 韩情脉脉
  • 哥斯拉
  • 林羽凡

COPYRIGHT © 2026 lifengdi.com. ALL RIGHTS RESERVED.

正在博友圈履约中

域名年龄

Theme Kratos+ By Dylan Li

津ICP备2024022503号-3

京公网安备11011502039375号