NullPointException
应该算是每一个程序员都很熟悉的?谁的代码不曾抛过几个空指针异常呢...
出现了NullPointException异常,虽然异常很常见,但有时候含义不明确,也会很头疼,那么如何用
Optional对null处理
全面认识下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("123","QingShanYuan")));
// 使用Optional.empty构造一个不代表任何对象的空Optional值
System.out.println(Optional.empty());
System.out.println(Optional.ofNullable(null));
System.out.println(Optional.ofNullable(new Content("222","QingShanYuanAnd")));
}
输出结果:
Optional[Content{id='111', value='JiaGouWuDao'}]
Optional.empty
Optional.empty
Optional[Content{id='222', value='JiaGouWuDao22'}]
这里需要注意下of
方法如果传入null会抛空指针异常,所以比较建议大家使用ofNullable
方法,可以省去调用前的额外判空操作,也可以避免无意中触发空指针问题:
/**
* Returns an {@code Optional} describing the given value, if
* non-{@code null}, otherwise returns an empty {@code Optional}.
*
* @param value the possibly-{@code null} value to describe
* @param <T> the type of the value
* @return an {@code Optional} with a present value if the specified value
* is non-{@code null}, otherwise an empty {@code Optional}
*/
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
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> ) |
看到这里的map
与flatMap
方法,肯定会联想到Stream
流(文章:stream流的高级玩法)
实际使用的时候,可以根据需要选择使用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);
}
注意:
map
与flatMap和stream中的方法类似,在stream中flatMap返回的还是个stream对象但map返回的是具体对象
从输出结果可以看出,两种不同的写法,实现是相同的效果:
Optional[Employee(employeeName=QingShanYuan)]
Optional[Employee(employeeName=QingShanYuan)]
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
,显然抛异常的含义更加明确。
总结
优雅的处理NullPointException,对空指针说再见