再见!该死的NullPointException
文章来源:【公众号:架构悟道】
目录
背景
null 的困扰
Optional 应对 null
使用抛异常替代 return null
JDK 与开源框架的实践
总结
背景
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 presentat 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 类提供了几个静态方法供对象的构建:
在项目中,我们可以选择使用上面的方法,实现 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 提供的一些方法:
看到这里的 map 与 flatMap 方法,不知道大家会不会联想到 Stream 流对象操作的时候也有这两个方法的身影呢(不了解的同学可以戳这个链接抓紧补补课:吃透 JAVA 的 Stream 流操作)?
的确,它们的作用也是类似的,都是用来将一个对象处理转换为另一个对象类型的:
对于 Optional 而言,map 与 flatMap 最终的实现效果其实都是一样的,仅仅只是入参的要求不一样,也即两种不同写法,两者区别点可以通过下图来理解:
实际使用的时候,可以根据需要选择使用 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 的一些应对处理策略与思路呢,这里就给大家分享到这里,希望可以对大家有所启发,通过不断的细节优化与改进,最终摆脱被空指针摆布的局面~
那么,对上面提到的一些内容与场景,你是否也有遇到相关的情况呢?你是怎么处理的呢?欢迎多切磋交流下~
此外:关于本文中涉及的演示代码的完整示例,我已经整理并提交到 github 中,如果您有需要,可以自取:
https://github.com/veezean/JavaBasicSkills
------------- END -------------
扫码免费获取600+页石杉老师原创精品文章汇总PDF原创技术文章汇总点个在看你最好看
再见!该死的NullPointException相关推荐
- 该死webgl_太该死了
该死webgl It's very hot and sticky here, I'm from the North, I don't recollect it ever getting close t ...
- 是时候优雅的和NullPointException说再见了
NullPointException应该算是每一个码农都很熟悉的家伙了吧?谁的代码不曾抛过几个空指针异常呢- 比如:你写了段如下的代码: public void getCompanyFromEmplo ...
- 荣耀30pro系统_荣耀30 pro:正式再见!
但随着市场竞争加剧,最近荣耀在价格上再次妥协,也是为了给新机开始让步,马上就是荣耀V40系列的发布会了,迭代荣耀30 pro+也不再坚挺,笔者从第三方平台上看到数据,荣耀30pro+这款手机大降520 ...
- 再见了,Python!!
结合我最近这些年的 Python 学习.开发经验,发现近90%的程序员在学 Python 时都会遇到下面这3个问题: 1.想学Python,但不知从何学起,应用方向太多了也不知道该选择什么方向... ...
- 再见,Python!
结合我最近这些年的 Python 学习.开发经验,发现近90%的程序员在学 Python 时都会遇到下面这3个问题: 1.想学Python,但不知从何学起,应用方向太多了也不知道该选择什么方向... ...
- 再见 Python,Hello Julia!
作者 | Rhea Moutafis 译者 | 苏本如,责编 | 夕颜 头图 | CSDN 下载自视觉中国 出品 | CSDN(ID:CSDNnews) 随着Python的停滞不前,一个新的热门竞争对 ...
- Python程序员Debug利器,和Print说再见 | 技术头条
整理 | Rachel 责编 | Jane 出品 | Python大本营(id:pythonnews) [导语]程序员每日都在和 debug 相伴.新手程序员需要学习的 debug 手段复杂多样,设置 ...
- 向iOS越狱彻底说再见!
老牌第三方软件商店 Cydia 关闭在即,iPhone 越狱时代又见落日归途? 作者 | 仲培艺 出品 | CSDN Cydia 线上商店是针对完成越狱的 iOS 设备的一种破解软件,在越狱过程中被装 ...
- 再见,Eclipse。
以下文章来源方志朋的博客,回复"666"获面试宝典 来源:cnblogs.com/ouyida3/p/9901312.html 使用了eclipse10年之后,我终于投向了IDEA ...
最新文章
- 检查用户显示器的分辨率
- mysql最高权限超级用户是_MySQL中,预设的、拥有最高权限超级用户的用户名为( )...
- RHEL 6.5 rpm包安装mplyer
- 利用Java针对MySql封装的jdbc框架类 JdbcUtils 完整实现(包含增删改查、JavaBean反射原理,附源码)...
- 所有锁的unlock要放到try{}finally{}里,不然发生异常返回就丢了unlock了
- 厉害了!JMeter 模拟超过 5 万的并发用户!
- Vue:打印组件PrintPage
- python选股并导入通达信自选股
- linux 网桥端口,linux网桥
- python 开发app应用
- 计算机毕设Python+Vue疫情期间优化旅游平台(程序+LW+部署)
- 繁體與簡體之間的轉化
- 01-Spring是什么?Spring有什么用?
- 终端天线—10.无线充线圈仿真
- 热敏电阻NTC、PTC
- 回调地狱终结者——Promise
- Aspose.PDF for Java 21.6 Crack
- linux开发板 wifi配置,iTOP-4412开发板Linux系统下使用wifi模块配置
- 解决www 54kk com/baidu劫持浏览器的问题
- 地方网站如何留住用户?