JAVA高级(二)——Optional
- 一、概述
- 二、为何要避免null指针
- 三、使用Optional优化null判断
- 四、小结
一、概述
1、null引用引发的问题,以及为什么要避免null引用
2、从null到Optional:以null安全的方式重写你的域模型
3、让Optional发光发热: 去除代码中对null的检查
4、读取Optional中可能值的几种方法
5、对可能缺失值的再思考
二、为何要避免null指针
其实根据有关资料显示,每个一程序的设计者们都会为 NullOpint
而苦恼,而且有大部分的运行调试的问题都会在 空指针
上面,所以接下来这篇文章就告诉大家如何去使用Optional
避免空指针;
代码一:
public class Person {
private Car car;
public Car getCar() { return car; }
}
public class Car {
private Insurance insurance;
public Insurance getInsurance() { return insurance; }
}
public class Insurance {
private String name;
public String getName() { return name; }
}
那么接下来这个代码有什么问题呢?
public String getCarInsuranceName(Person person) {
return person.getCar().getInsurance().getName();
}
其实如果其中一个出现了空,那么这个代码就会报错,程序不能正常进行运行;
2.1 使用if-else
所以我们可以将代码改为:防御的方式进行避免空指针
第一种方式:
public String getCarInsuranceName(Person person) {
if (person != null) { (以下5行)每个null检查都会增加调用链上剩余代码的嵌套层数
Car car = person.getCar();
if (car != null) {
Insurance insurance = car.getInsurance();
if (insurance != null) {
return insurance.getName();
}
}
}
return "Unknown";
}
防御模式二:采用每个退出节点都进行判断
public String getCarInsuranceName(Person person) {
if (person == null) { (以下9行)每个null检查都会添加新的退出点
return "Unknown";
}
Car car = person.getCar();
if (car == null) {
return "Unknown";
}
Insurance insurance = car.getInsurance();
if (insurance == null) {
return "Unknown";
}
return insurance.getName();
}
经过上述if else
的一顿操作,是不感觉代码非的不美观,庆幸java提供一个判断为空的类,那个就是Optional。接下来我们会说明如何正确的使用Optional类。
三、使用Optional优化null判断
3.1 Optional 入门
这里Optional
就像是一个容器,里面放一个泛型,
- 如果泛型对象为空,那么这个
Optional<T>
就是 null
; - 否者,可以调用
Optional
中的方法进行操作里面的对象
元素;(接下里会具体介绍Optional
中的方法)
变量存在时,Optional类只是对类简单封装。变量不存在时,缺失的值会被建模成一个“空”的Optional对象,由方法Optional.empty()返回。Optional.empty()方法是一个静态工厂方法,它返回Optional类的特定单一实例。
你可能还有疑惑,null引用和Optional.empty()有什么本质的区别吗?
从语义上讲,你可以把它们当作一回事儿,但是实际中它们之间的差别非常大:如果你尝试解引用一个null,那么一定会触发NullPointerException,不过使用Optional.empty()就完全没事儿(
只是创建了一个相当于仓库(Optional)的对象,如果仓库没有货物就只会返回一个 Null的Optional,并不会使仓库无法正常运转
),它是Optional类的一个有效对象,多种场景都能调用,非常有用。关于这一点,接下来的部分会详细介绍。
3.1.1 使用Optional优化Car类
既然有了上面的说明,接下来我们可以优化我们实体类Car
对象,让其被仓库对象(Optional
)包裹,到达优化null
的效果;
public class Person {
private Optional<Car> car; ←---- 人可能有汽车,也可能没有汽车,因此将这个字段声明为Optional
public Optional<Car> getCar() { return car; }
}
public class Car {
private Optional<Insurance> insurance; ←---- 汽车可能进行了保险,也可能没有保险,所以将这个字段声明为Optional
public Optional<Insurance> getInsurance() { return insurance; }
}
public class Insurance {
private String name; ←---- 保险公司必须有名字
public String getName() { return name; }
}
代码中person
引用的是Optional<Car>
,而car引用的是Optional<Insurance>
,这种方式非常清晰地表达了你的模型中一个person
可能拥有也可能没有car
的情形;同样,car
可能进行了保险,也可能没有保险。
我们看到
insurance
公司的名称被声明成String
类型,而不是Optional<String>
,这非常清楚地表明声明为insurance
公司的类型必须提供公司名称。使用这种方式,一旦解引用insurance
公司名称时发生NullPointerException
,你就能非常确定地知道出错的原因,不再需要为其添加null的检查,因为null
的检查只会掩盖问题,并未真正地修复问题。insurance
公司必须有个名称,所以,如果你遇到一个公司没有名称,你需要调查你的数据出了什么问题,而不应该再添加一段代码,将这个问题隐藏。所以
Optional
能够更直观的反应问题,并且提醒你解决问题;
由于Optional
并没有实现 序列化操作
,所以如果在正式项目中的实体类中使用上述改良代码可能不妥,所以接下来我们会说明另一种解决方式;
3.1.2 Optional的几种模式
到目前为止,一切都很顺利。你已经知道了如何使用Optional类型来声明你的域模型,也了解了这种方式与直接使用null引用表示变量值的缺失的优劣。但是,该如何使用呢?用这种方式能做什么,或者怎样使用Optional封装的值呢?
- 声明一个空的
Optional
正如前文所述,你可以通过静态工厂方法Optional.empty
创建一个空的Optional
对象:
Optional<Car> optCar = Optional.empty();
- 依据一个非空值创建Optional
你还可以使用静态工厂方法Optional.of依据一个非空值创建一个Optional对象:
Optional<Car> optCar = Optional.of(car);
- 可接受null的Optional
最后,使用静态工厂方法Optional.ofNullable
,你可以创建一个允许null
值的Optional
对象:
Optional<Car> optCar = Optional.ofNullable(car);
3.1.3 使用map从Optional中提取值
从对象中提取信息是一种比较常见的模式。比如,你可能想要从insurance
公司对象中提取公司的名称。提取名称之前,你需要检查insurance
对象是否为null
,代码如下所示:
String name = null;
if(insurance != null){
name = insurance.getName();
}
为了支持这种模式,Optional提供了一个map方法。
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);
从概念上看,这与stream
流的map
方法相差无几。map
操作会将提供的函数应用于流的每个元素。你可以把Optional
对象看成一种特殊的集合数据,它至多包含一个元素。如果Optional
包含一个值,那函数就将该值作为参数传递给map
,对该值进行转换。如果Optional
为空,就什么也不做。下图对这种相似性进行了说明,展示了把一个将正方形转换为三角形的函数,分别传递给正方形和Optional
正方形流的map
方法之后的结果。(Stream和Optional的map方法对比)
但是如何重构下面代码呢?
public String getCarInsuranceName(Person person) {
return person.getCar().getInsurance().getName();
}
接下来我们要使用FlatMap
方法
3.1.4 使用flatMap链接Optional对象
刚开始学到map之后呢,我们会产生一个想法,代码如下:
Optional<Person> optPerson = Optional.of(person);
Optional<String> name =
optPerson.map(Person::getCar)
.map(Car::getInsurance)
.map(Insurance::getName);
但是这样就会照成了对象的嵌套Optional<Optional<Car>>
,以至于无法通过编译;所以map
是无法满足对象里面获取对象的需求的,这时候我们的FlatMap
就出现了。
???? 使用两层的Optional
对象
flatMap
方法。使用流时,flatMap
方法接受一个函数作为参数,这个函数的返回值是另一个流。这个方法会应用到流中的每一个元素,最终形成一个新的流的流。但是flagMap
会用流的内容替换每个新生成的流。换句话说,由方法生成的各个流会被合并或者扁平化为一个单一的流。这里你希望的结果其实也是类似的,但是你想要的是将两层的Optional
合并为一个。
Stream
和 Optional
的 FlatMap
对比
如上图可以看出,
-
Stream
流:就是将对象进行了转换,以至于对象一致性; -
Optional
中的 FlatMap
:是将Optional
中的对象进行取出(正方形),然后再转换成一个新的对象(三角形),最后放入Optional
(仓库中)
3.1.4.1 使用Optional获取car的保险公司名称
使用FlatMap
进行重写
public String getCarInsuranceName(Optional<Person> person) {
return person.flatMap(Person::getCar)
.flatMap(Car::getInsurance)
.map(Insurance::getName)
.orElse("Unknown"); ←---- 如果Optional的结果值为空,设置默认值
}
3.1.4.2 使用Optional解引用串接的Person/Car/Insurance对象
由Optional<Person>
对象,我们可以结合使用之前介绍的map
和flatMap
方法,从Person
中解引用出Car
,从Car
中解引用出Insurance
,从Insurance
对象中解引用出包含insurance
公司名称的字符串。下图进行了说明:
3.1.5 操作由Optional对象构成的Stream流
Java 9
引入了Optional
的stream()
方法,使用该方法可以把一个含值的Optional对象转换成由该值构成的Stream对象,或者把一个空的Optional对象转换成等价的空Stream。这一技术为典型流处理场景带来了极大的便利:当你要处理的对象是由Optional对象构成的Stream时,你需要将这个Stream转换为由原Stream中非空Optional对象值组成的新Stream。本节会通过一个实际例子演示为什么你需要处理由Optional对象构成的Stream,以及如何执行这种操作。
接下来一个例子说明 Optional
中 Stream
流怎么用:
业务场景:找出person
列表所使用的保险公司名称(不含重复项)
public Set<String> getCarInsuranceNames(List<Person> persons) {
return persons.stream()
.map(Person::getCar)
.map(optCar -> optCar.flatMap(Car::getInsurance))
.map(optIns -> optIns.map(Insurance::getName))
.flatMap(Optional::stream)
.collect(toSet());
}
例子讲解:
- 将
persons
转换为 stream
-> Stream<Person>
; - 通过第一个
map
将数据转换为:Optional<Stream<Car>>
; - 第二个
map
对每个Optional<Car>
执行flatMap
操作,将其转换成对应的Optional<Insurance>
对象 - 第三个
map
将每一个 Optional<Insurance>
执行 flatMap
操作将 Optional<Insurance>
转换为Optional<String>
; - 使用
flatMap
将Stream<Optional<String>>
转换为Stream<String>
对象,只保留流中那些存在保险公司名的对象; - 收集成为
set
集合,防止重复。
注意:
这时候你可以预防空安全(null-safe)问题。然而却碰到了新问题。怎样去除那些
空的Optional对象
,解包出其他对象的值,并把结果保存到集合Set
中呢?我们就可以使用 stream.filter
进行操作咯!
Stream<Optional<String>> stream = persons.stream()
.map(Person::getCar)
.map(optCar -> optCar.flatMap(Car::getInsurance))
.map(optIns -> optIns.map(Insurance::getName))
Set<String> result = stream.filter(Optional::isPresent)
.map(Optional::get)
.collect(toSet());
所以这里的代码就是将Optional为空的数据进行过滤,然后再进行收集符合条件的保险名;
3.1.6 默认行为及解引用Optional对象
我们决定采用orElse
方法读取这个变量的值,使用这种方式你还可以定义一个默认值,当遭遇空的Optional
变量时,默认值会作为该方法的调用返回值。Optional
类提供了多种方法读取Optional
实例中的变量值。
-
get()
是这些方法中最简单但又最不安全的方法。如果变量存在,那它直接返回封装的变量值,否则就抛出一个NoSuchElementException
异常。所以,除非你非常确定Optional
变量一定包含值,否则使用这个方法是个相当糟糕的主意。此外,这种方式即便相对于嵌套式的null检查,也并未体现出多大的改进。 -
orElse(T other)
它允许你在Optional对象不包含值时提供一个默认值。 -
orElseGet(Supplier<? extends="" t=""?> other)
是orElse
方法的延迟调用版,因为Supplier
方法只有在Optional
对象不含值时才执行调用。如果创建默认值是件耗时费力的工作,你应该考虑采用这种方式(借此提升程序的性能),或者你需要非常确定某个方法仅在Optional
为空时才进行调用,也可以考虑该方式(使用orElseGet时至关重要)。 -
or(Supplier<? extends=""?><? extends="" t=""?>> supplier)
与前面介绍的orElseGet方法很像,不过它不会解包Optional
对象中的值,即便该值是存在的。实战中,如果Optional对象含有值,这一方法(自Java 9引入)不会执行任何额外的操作,直接返回该Optional对象。如果原始Optional
对象为空,该方法会延迟地返回一个不同的Optional
对象。 -
orElseThrow(Supplier<? extends="" x=""?> exceptionSupplier)
和get方法非常类似,它们遭遇Optional对象为空时都会抛出一个异常,但是使用orElseThrow
你可以定制希望抛出的异常类型。 -
ifPresent(Consumer<? super="" t=""?>consumer)
变量值存在时,执行一个以参数形式传入的方法,否则就不进行任何操作。
3.1.7 两个Optional对象的组合
现在,假设你有这样一个方法,它接受一个Person和一个Car对象,并以此为条件对外部提供的服务进行查询,通过一些复杂的业务逻辑,试图找到满足该组合的最便宜的保险公司:
public Insurance findCheapestInsurance(Person person, Car car) {
// 不同的保险公司提供的查询服务
// 对比所有数据
return cheapestCompany;
}
这时我们可以想一下如何去完成这个能预防null-
的代码呢?所以我们可以引入Opional
,将两个传入的对象进行包装一下;
public Optional<Insurance> nullSafeFindCheapestInsurance(
Optional<Person> person, Optional<Car> car) {
if (person.isPresent() && car.isPresent()) {
return Optional.of(findCheapestInsurance(person.get(), car.get()));
} else {
return Optional.empty();
}
}
这个方法具有明显的优势,从它的签名就能非常清楚地知道无论是person还是car,它的值都有可能为空,出现这种情况时,方法的返回值也不会包含任何值。不幸的是,该方法的具体实现和你之前曾经实现的null检查太相似了:方法接受一个Person和一个Car对象作为参数,而二者都有可能为null。利用Optional类提供的特性,有没有更好或更地道的方式来实现这个方法呢?
那么接下来,我们继续优化自己的代码:
public Optional<Insurance> nullSafeFindCheapestInsurance(
Optional<Person> person, Optional<Car> car) {
return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));
}
执行流程:
-
flatMap
判断person
是否为空,如果为空就不执行; - 如果
person
存在,这次调用就会将其作为一个Function
进行传入,并按照与flatMap方法的约定返回Optional<Insurance>
对象 ; - 这个函数的函数体会对第二个
Optional
对象执行map
操作,如果第二个对象不包含car
,函数Function
就返回一个空的Optional
对象,整个nullSafeFindCheapestInsurance
方法的返回值也是一个空的Optional
对象。 - 最后,如果
person
和car
对象都存在,那么作为参数传递给map
方法的Lambda
表达式就能够使用这两个值安全地调用原始的findCheapestInsurance
方法,完成期望的操作。
3.1.8 使用Filter进行剔除
例如:我们检查公司名字是否为“xiao company”
。为了以一种安全的方式进行操作,所以我们可以需要判断这个名字是否为null,代码如下
Insurance insurance = ...;
if(insurance != null && "xiao company".equals(insurance.getName())){
System.out.println("ok");
}
使用Optional
的filter
进行重构代码:
Optional<Insurance> optInsurance = ...;
optInsurance.filter(insurance ->
"xiao company".equals(insurance.getName()))
.ifPresent(x -> System.out.println("ok"));
filter
方法接受一个谓词作为参数。如果Optional
对象的值存在,并且它符合谓词的条件,filter
方法就返回其值;否则它就返回一个空的Optional
对象。如果你还记得我们可以将Optional
看成最多包含一个元素的Stream
对象,这个方法的行为就非常清晰了。如果Optional
对象为空,那它不做任何操作,反之,它就对Optional
对象中包含的值施加谓词操作。如果该操作的结果为true
,那它不做任何改变,直接返回该Optional
对象,否则就将该值过滤掉,
Optional类的方法
四、小结
- null引用在历史上被引入到程序设计语言中,目的是为了表示变量值的缺失。
- Java 8中引入了一个新的类java.util.Optional,对存在或缺失的变量值进行建模。
- 你可以使用静态工厂方法Optional.empty、Optional.of以及Optional.ofNullable创建Optional对象。
- Optional类支持多种方法,比如map、flatMap、filter,它们在概念上与Stream类中对应的方法十分相似。
- 使用Optional会迫使你更积极地解引用Optional对象,以应对变量值缺失的问题,最终,你能更有效地防止代码中出现不期而至的空指针异常。