是时候优雅的和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,可以按照具体的场景分开区别对待:
- 确定是因为代码或者逻辑层面处理错误导致的无值,通过throw异常的方式,强制调用方感知并进行处理对待
- 如果null代表业务上的一种正常可选值,可以考虑返回Optional来替代。
当然咯,有时候即使我们自己的代码不返回null
,也难免会遇到调用别人的接口返回null的情况,这种时候我们真的就只能不停的去判空来保护自己吗?有没有更优雅的应对策略来避免自己掉坑呢?下面呢,我们一起探讨下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()
方法,如果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提供的一些方法:
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
流对象操作的时候也有这两个方法的身影呢(不了解的同学可以戳这个链接抓紧补补课:吃透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
判空操作要舒服多了?