再见!该死的NullPointException
The following article is from 架构悟道 Author veezean
背景
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;
}
作为 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);
}
null 的困扰
通过上面代码示例,我们可以发现使用 null 可能会带来的一系列困扰:
空指针异常,导致代码运行时变得不可靠,稍不留神可能就崩了
使代码膨胀,导致代码中充斥大量的 null 检查与保护,使代码可读性降低
所以说,一个比较好的编码习惯,是尽量避免在程序中使用 null,可以按照具体的场景分开区别对待:
确定是因为代码或者逻辑层面处理错误导致的无值,通过 throw 异常的方式,强制调用方感知并进行处理对待
如果 null 代表业务上的一种正常可选值,可以考虑返回 Optional 来替代。
当然咯,有时候即使我们自己的代码不返回 null,也难免会遇到调用别人的接口返回 null 的情况,这种时候我们真的就只能不停的去判空来保护自己吗?
有没有更优雅的应对策略来避免自己掉坑呢?下面呢,我们一起探讨下 null 的一些优雅应对策略。
Optional 应对 null
| Optional 一定比 return null 安全吗
前面我们提到了说使用 Optional 来替代 null,减少调用端的判空操作压力,防止调用端出现空指针异常。
那么,使用返回 Optional 对象就一定会比 return null 更靠谱吗?答案是:也不一定,关键要看怎么用!
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)
-------上面代码会报异常--------
public void testCallOptional2() {
Optional<Content> optional = getContent();
// 使用前先判断下元素是否存在
if (optional.isPresent()) {
Content content = optional.get();
System.out.println(content);
}
}
public void testNullReturn2() {
Content content = getContent2();
if (content != null) {
System.out.println(content.getValue());
}
}
那怎么样才是正确的使用方式呢,下面一起来看下。
| 全面认识下 Optional
①创建 Optional 对象
Optional<T> 对象,可以用来表示一个 T 类型对象的封装,或者也可以表示不是任何对象。
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'}]
| Optional 常用方法理解
看到这里的 map 与 flatMap 方法,不知道大家会不会联想到 Stream 流对象操作的时候也有这两个方法的身影呢(不了解的同学可以戳这个链接抓紧补补课:吃透 JAVA 的 Stream 流操作)?
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 判空操作要舒服多了?
适用场景:需要通过某个比较长的调用链路一层一层去调用获取某个值的时候,使用上述方法,可以避免空指针以及减少冗长的判断逻辑。
②需要有值兜底的数据获取场景
编码的时候,经常会遇到一些数据获取的场景,需要先通过一些处理逻辑尝试获取一个数据,如果没有获取到需要的数据,还需要返回一个默认值,或者是执行另一处理逻辑继续尝试获取。
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;
}
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,这个方法作为项目中被高频调用的一个方法,意味着所有的调用端都必须要做判空保护。
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 进行封装之后有两个明显的优势:
强烈的业务属性说明,明确的让人知晓这个是一个可选字段,等同于数据库建表语句里面设置 nullable 标识一样的效果。
调用端使用的时候也省去了判空操作。
适用场景:数据实体定义的时候,对于可选参数,采用 Optional 封装类型替代。
使用抛异常替代 return null
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 的情况。
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;
}
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 的一些应对处理策略与思路呢,这里就给大家分享到这里,希望可以对大家有所启发,通过不断的细节优化与改进,最终摆脱被空指针摆布的局面~
那么,对上面提到的一些内容与场景,你是否也有遇到相关的情况呢?你是怎么处理的呢?欢迎多切磋交流下~
https://github.com/veezean/JavaBasicSkills
PS:防止找不到本篇文章,可以收藏点赞,方便翻阅查找哦。
往期推荐
替代ELK:ClickHouse+Kafka+FlieBeat
面试官:如何防止你的 jar 包被反编译?
DataGrip 2022.2 新版来袭,界面更大气了
Spring Batch批处理框架,真心强啊!!
如何设计一个高质量的 API 接口
VS Code Java 7 月更新:Lombok 支持重大提升, Spring 增强新功能!
使用uuid做MySQL主键,被老板,爆怼一顿!