11.3 应用Optional的几种模式
到目前为止,一切都很顺利。你已经知道了如何使用Optional类型来声明你的域模型,也了解了这种方式与直接使用null引用表示变量值的缺失的优劣。但是,该如何使用呢?用这种方式能做什么,或者怎样使用Optional封装的值呢?
11.3.1 创建Optional对象
使用Optional之前,你首先需要学习的是如何创建Optional对象。完成这一任务有多种方法。
- 声明一个空的
Optional
正如前文所述,你可以通过静态工厂方法Optional.empty创建一个空的Optional对象:
Optional<Car> optCar = Optional.empty();
- 依据一个非空值创建
Optional
你还可以使用静态工厂方法Optional.of依据一个非空值创建一个Optional对象:
Optional<Car> optCar = Optional.of(car);
如果car是一个null,这段代码就会立即抛出一个NullPointerException,而不是等到你试图访问car的属性值时才返回一个错误。
- 可接受
null的Optional
最后,使用静态工厂方法Optional.ofNullable,你可以创建一个允许null值的Optional对象:
Optional<Car> optCar = Optional.ofNullable(car);
如果car是null,那么得到的Optional对象就是个空对象。
你可能已经猜到,我们还需要继续研究“如何获取Optional变量中的值”。尤其是,Optional提供了一个get方法,它能非常精准地完成这项工作,后面会详细介绍这部分内容。不过get方法在遭遇到空的Optional对象时也会抛出异常,所以不按照约定的方式使用它,又会让我们再度陷入由null引起的代码维护的梦魇。因此,我们首先从无须显式检查的Optional值的使用入手,这些方法与Stream中的某些操作极其相似。
11.3.2 使用map从Optional对象中提取和转换值
从对象中提取信息是一种比较常见的模式。比如,你可能想要从insurance公司对象中提取公司的名称。提取名称之前,你需要检查insurance对象是否为null,代码如下所示:
String name = null;if(insurance != null){name = insurance.getName();}
为了支持这种模式,Optional提供了一个map方法。它的工作方式如下(这里继续借用了代码清单11-4的模式):
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);Optional<String> name = optInsurance.map(Insurance::getName);
从概念上看,这与我们在第4章和第5章中看到的流的map方法相差无几。map操作会将提供的函数应用于流的每个元素。你可以把Optional对象看成一种特殊的集合数据,它至多包含一个元素。如果Optional包含一个值,那函数就将该值作为参数传递给map,对该值进行转换。如果Optional为空,就什么也不做。图11-2对这种相似性进行了说明,展示了把一个将正方形转换为三角形的函数,分别传递给正方形和Optional正方形流的map方法之后的结果。

图 11-2 Stream和Optional的map方法对比
这看起来挺有用,但是你怎样才能应用起来,重构代码清单11-1的代码呢?那段代码里用安全的方式链接了多个方法。
public String getCarInsuranceName(Person person) {return person.getCar().getInsurance().getName();}
为了达到这个目的,需要求助Optional提供的另一个方法flatMap。
11.3.3 使用flatMap链接Optional对象
由于我们刚刚学习了如何使用map,你的第一反应可能是可以利用map重写之前的代码,如下所示:
Optional<Person> optPerson = Optional.of(person);Optional<String> name =optPerson.map(Person::getCar).map(Car::getInsurance).map(Insurance::getName);
不幸的是,这段代码无法通过编译。为什么呢?optPerson是Optional类型的变量,调用map方法应该没有问题。但getCar返回的是一个Optional类型的对象(如代码清单11-4所示),这意味着map操作的结果是一个Optional类型的对象。因此,它对getInsurance的调用是非法的,因为最外层的Optional对象包含了另一个Optional对象的值,而它当然不会支持getInsurance方法。图11-3说明了你会遭遇的嵌套式Optional结构。

图 11-3 两层的Optional对象
所以,该如何解决这个问题呢?让我们再回顾一下你之前在流上使用过的模式:flatMap方法。使用流时,flatMap方法接受一个函数作为参数,这个函数的返回值是另一个流。这个方法会应用到流中的每一个元素,最终形成一个新的流的流。但是flagMap会用流的内容替换每个新生成的流。换句话说,由方法生成的各个流会被合并或者扁平化为一个单一的流。这里你希望的结果其实也是类似的,但是你想要的是将两层的Optional合并为一个。
跟图11-2类似,我们借助图11-4来说明flatMap方法在Stream和Optional类之间的相似性。

图 11-4 Stream和Optional的flagMap方法对比
这里传给流的flatMap方法的函数,会转换每个正方形到一个包含两个三角形的流中。如果将该函数应用于简单的map,那么map结果将是包含了其他三个流的流,这三个流都分别包含两个三角形。如果该函数应用于flatMap方法,结果则不一样,flatMap会捋平两层结构的流为总计包含六个三角形的单层流。类似地,传递给Optional的flatMap方法的函数,会转换原始Optional中的正方形到一个Optional中(包含一个三角形)。如果函数传递给map方法,那么map结果是包含了一个Optional的Optional,相应地,最里层的Optional包含一个三角形。但flatMap会捋平两层结构的Optional为一个包含了一个三角形的单层结构Optional。
- 使用
Optional获取car的保险公司名称
相信现在你已经对Optional的map和flatMap方法有了一定的了解,让我们看看如何应用。代码清单11-2和代码清单11-3的示例用基于Optional的数据模式重写之后,如代码清单11-5所示。
代码清单 11-5 使用
Optional获取car的insurance名称
public String getCarInsuranceName(Optional<Person> person) {return person.flatMap(Person::getCar).flatMap(Car::getInsurance).map(Insurance::getName).orElse("Unknown"); ←---- 如果Optional的结果值为空,设置默认值}
通过比较代码清单11-5和之前的两个代码清单,可以看到,处理潜在可能缺失的值时,使用Optional具有明显的优势。这一次,你可以用非常容易却又普适的方法实现之前你期望的效果——不再需要使用那么多的条件分支,也不会增加代码的复杂性。
从具体的代码实现来看,首先我们注意到你修改了代码清单11-2和代码清单11-3中的getCarInsuranceName方法的签名,因为我们很明确地知道存在这样的用例,即一个不存在的Person被传递给了方法,比如,Person是使用某个标识符从数据库中查询出来的,你想要对数据库中不存在指定标识符对应的用户数据的情况进行建模。你可以将方法的参数类型由Person改为Optional,对这种特殊情况进行建模。
我们再一次看到了这种方式的优点,它通过类型系统让你的域模型中隐藏的知识显式地体现在你的代码中,换句话说,你永远都不应该忘记语言的首要功能就是沟通,即使对程序设计语言而言也没有什么不同。声明方法接受一个Optional参数,或者将结果作为Optional类型返回,让你的同事或者未来你方法的使用者,很清楚地知道它可以接受空值,或者可能返回一个空值。
- 使用
Optional解引用串接的Person/Car/Insurance对象
由Optional对象,我们可以结合使用之前介绍的map和flatMap方法,从Person中解引用出Car,从Car中解引用出Insurance,从Insurance对象中解引用出包含insurance公司名称的字符串。图11-5对这种流水线式的操作进行了说明。

图 11-5 使用Optional解引用串接的Person/Car/Insurance
这里,我们从以Optional封装的Person入手,对其调用flatMap(Person::getCar)。如前所述,这种调用逻辑上可以划分为两步。第一步,某个Function作为参数,被传递给由Optional封装的Person对象,对其进行转换。在这个场景中,Function的具体表现是一个方法引用,即对Person对象的getCar方法进行调用。由于该方法返回一个Optional类型的对象,因此Optional内的Person也被转换成了这种对象的实例,结果就是一个两层的Optional对象,最终它们会被flagMap操作合并。从纯理论的角度而言,你可以将这种合并操作简单地看成把两个Optional对象结合在一起,如果其中有一个对象为空,就构成一个空的Optional对象。如果你对一个空的Optional对象调用flatMap,那实际情况又会如何呢?结果不会发生任何改变,返回值也是个空的Optional对象。与此相反,如果Optional封装了一个Person对象,传递给flapMap的Function,就会应用到Person上对其进行处理。在这个例子中,由于Function的返回值已经是一个Optional对象,因此flapMap方法就直接将其返回了。
第二步与第一步大同小异,它会将Optional转换为Optional。第三步则会将Optional转化为Optional对象,由于Insurance.getName()方法的返回类型为String,这里就不再需要进行flapMap操作了。
截至目前,返回的Optional可能是两种情况:如果调用链上的任何一个方法返回一个空的Optional,那么结果就为空,否则返回的值就是你期望的保险公司的名称。那么,你如何读出这个值呢?毕竟你最后得到的这个对象还是个Optional,它可能包含保险公司的名称,也可能为空。代码清单11-5使用了一个名为orElse的方法,当Optional的值为空时,它会为其设定一个默认值。除此之外,还有很多其他的方法可以为Optional设定默认值,或者解析出Optional代表的值。接下来我们会对此做进一步的探讨。
在域模型中使用
Optional,以及为什么它们无法序列化在代码清单11-4中,我们展示了如何在你的域模型中使用
Optional,将允许缺失或者暂无定义的变量值用特殊的形式标记出来。然而,Optional类设计者的初衷并非如此,他们构思时怀揣的是另一个用例。这一点,Java语言的架构师Brian Goetz曾经非常明确地陈述过,Optional的设计初衷仅仅是要支持能返回Optional对象的语法。由于
Optional类设计时就没特别考虑将其作为类的字段使用,因此它也并未实现Serializable接口。由于这个原因,如果你的应用使用了某些要求序列化的库或者框架,在域模型中使用Optional,有可能引发应用程序故障。然而,我们相信,通过前面的介绍,你已经看到用Optional声明域模型中的某些类型是个不错的主意,尤其是你需要遍历有可能全部或部分为空,或者可能不存在的对象时。如果你一定要实现序列化的域模型,作为替代方案,建议你像下面这个例子那样,提供一个能访问声明为Optional、变量值可能缺失的接口,如下所示:
public class Person {private Car car;public Optional<Car> getCarAsOptional() {return Optional.ofNullable(car);}}
11.3.4 操纵由Optional对象构成的Stream
Java 9引入了Optional的stream()方法,使用该方法可以把一个含值的Optional对象转换成由该值构成的Stream对象,或者把一个空的Optional对象转换成等价的空Stream。这一技术为典型流处理场景带来了极大的便利:当你要处理的对象是由Optional对象构成的Stream时,你需要将这个Stream转换为由原Stream中非空Optional对象值组成的新Stream。本节会通过一个实际例子演示为什么你需要处理由Optional对象构成的Stream,以及如何执行这种操作。
代码清单11-6的例子使用了代码清单11-4中定义的领域模型Person/Car/Insurance。假设你需要实现一个方法,该方法接受一个由Person构成的列表List,返回该列表中拥有一辆汽车的人所使用的保险公司名称集合Set。
代码清单 11-6 找出
person列表所使用的保险公司名称(不含重复项)
public Set<String> getCarInsuranceNames(List<Person> persons) {return persons.stream().map(Person::getCar) ←---- 将person列表转换为Optional<Car>组成的流,car是列表中person名下的汽车.map(optCar -> optCar.flatMap(Car::getInsurance)) ←---- 对每个Optional<Car>执行flatMap操作,将其转换成对应的Optional<Insurance>对象.map(optIns -> optIns.map(Insurance::getName)) ←---- 将每个Optional<Insurance>映射成包含对应保险公司名字的Optional<String>.flatMap(Optional::stream) ←---- 将Stream<Optional<String>>转换为Stream<String>对象,只保留流中那些存在保险公司名的对象.collect(toSet()); ←---- 收集处理的结果字符串,将其保存到一个不含重复值的Set中}
很多时候,操纵流元素都需要链接一长串的转换、过滤或者其他的操作,现在处理的复杂度又进一步增大了,因为每个元素都会被封装到Optional对象中。还记得么,建模时我们假设person对象可能没有汽车,执行getCar()方法时,它返回的是一个Optional对象,而不是一个简单的Car对象。因此,第一次map转换之后,你得到的是一个由Optional对象构成的Stream。接下来的两个map操作帮助你将Optional转换成了Optional,接着转换成了Optional。这些跟你在代码清单11-5中所做的几乎一样,唯一的区别是现在是对Stream中的元素进行操作,而之前是对单一元素进行操作。
这三个转换操作之后,你得到了一个Stream对象,这些Optional对象中的一些可能为空,因为有的人可能并没有汽车,或者有汽车但是没有投保。使用Optional,即便是碰到了值缺失的情况,你也不需要再为这些操作是否“空安全”(null-safe)而烦心了。然而,你现在碰到了新的问题,怎样去除那些空的Optional对象,解包出其他对象的值,并把结果保存到集合Set中呢?当然,你可以像下面这样,使用filter和map得到最终的结果:
Stream<Optional<String>> stream = ...Set<String> result = stream.filter(Optional::isPresent).map(Optional::get).collect(toSet());
不过,正如我们在代码清单11-6中所预见的,采用Optional类的stream()方法完全可以只通过一次,而不是两次操作达到同样的效果。实际上,这个方法会依据要转换的Optional对象是否为空,将每个Optional对象转换为含有零个或一个元素的流。基于这一原理,对该方法的引用可以看成是从流的一个单一元素向另一个流的单一元素执行转换,结果传递回原始流执行flatMap方法调用。我们已经知道,通过这种方式每个元素都被转换成了流,最初两层由流组成的流结构经过转换简化为单层的流。通过这一技巧,你可以解包Optional对象,提取其中的值,跳过那些空的对象,所有这一切都只需执行一次操作。
11.3.5 默认行为及解引用Optional对象
我们决定采用orElse方法读取这个变量的值,使用这种方式你还可以定义一个默认值,当遭遇空的Optional变量时,默认值会作为该方法的调用返回值。Optional类提供了多种方法读取Optional实例中的变量值。
get()是这些方法中最简单但又最不安全的方法。如果变量存在,那它直接返回封装的变量值,否则就抛出一个NoSuchElementException异常。所以,除非你非常确定Optional变量一定包含值,否则使用这个方法是个相当糟糕的主意。此外,这种方式即便相对于嵌套式的null检查,也并未体现出多大的改进。orElse(T other)是我们在代码清单11-5中使用的方法,正如之前提到的,它允许你在Optional对象不包含值时提供一个默认值。orElseGet(Supplier other)是orElse方法的延迟调用版,因为Supplier方法只有在Optional对象不含值时才执行调用。如果创建默认值是件耗时费力的工作,你应该考虑采用这种方式(借此提升程序的性能),或者你需要非常确定某个方法仅在Optional为空时才进行调用,也可以考虑该方式(使用orElseGet时至关重要)。or(Supplier> supplier)与前面介绍的orElseGet方法很像,不过它不会解包Optional对象中的值,即便该值是存在的。实战中,如果Optional对象含有值,这一方法(自Java 9引入)不会执行任何额外的操作,直接返回该Optional对象。如果原始Optional对象为空,该方法会延迟地返回一个不同的Optional对象。orElseThrow(Supplier exceptionSupplier)和get方法非常类似,它们遭遇Optional对象为空时都会抛出一个异常,但是使用orElseThrow你可以定制希望抛出的异常类型。ifPresent(Consumerconsumer)变量值存在时,执行一个以参数形式传入的方法,否则就不进行任何操作。
Java 9还引入了一个新的实例方法:
ifPresentOrElse(Consumer action, Runnable emptyAction)。该方法不同于ifPresent,它接受一个Runnable方法,如果Optional对象为空,就执行该方法所定义的动作。
11.3.6 两个Optional对象的组合
现在,假设你有这样一个方法,它接受一个Person和一个Car对象,并以此为条件对外部提供的服务进行查询,通过一些复杂的业务逻辑,试图找到满足该组合的最便宜的保险公司:
public Insurance findCheapestInsurance(Person person, Car car) {// 不同的保险公司提供的查询服务// 对比所有数据return cheapestCompany;}
我们还假设你想要该方法的一个null-安全的版本,它接受两个Optional对象作为参数,返回值是一个Optional对象,如果传入的任何一个参数值为空,它的返回值亦为空。Optional类还提供了一个isPresent方法,如果Optional对象包含值,该方法就返回true,所以你的第一想法可能是通过下面这种方式实现该方法:
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类提供的特性,有没有更好或更地道的方式来实现这个方法呢?花几分钟时间思考一下测验11.1,试试能不能找到更优雅的解决方案。
测验11.1:以不解包的方式组合两个
Optional对象结合本节中介绍的
map和flatMap方法,用一行语句重新实现之前出现的nullSafeFindCheapestInsurance()方法。答案:你可以像使用三元操作符那样,无须任何条件判断的结构,以一行语句实现该方法,代码如下。
public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Car> car) {return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));}在这段代码中,你对第一个
Optional对象调用flatMap方法,如果它是个空值,传递给它的Lambda表达式就不会执行,这次调用会直接返回一个空的Optional对象。反之,如果person对象存在,这次调用就会将其作为函数Function的输入,并按照与flatMap方法的约定返回一个Optional对象。这个函数的函数体会对第二个Optional对象执行map操作,如果第二个对象不包含car,函数Function就返回一个空的Optional对象,整个nullSafeFindCheapestInsurance方法的返回值也是一个空的Optional对象。最后,如果person和car对象都存在,那么作为参数传递给map方法的Lambda表达式就能够使用这两个值安全地调用原始的findCheapestInsurance方法,完成期望的操作。
Optional类和Stream接口的相似之处远不止map和flatMap这两个方法。还有第三个方法filter,它的行为在两种类型之间也极其相似,接下来的一节会对此进行介绍。
11.3.7 使用filter剔除特定的值
你经常需要调用某个对象的方法,查看它的某些属性。比如,你可能需要检查保险公司的名称是否为“Cambridge-Insurance”。为了以一种安全的方式进行这些操作,你首先需要确定引用指向的Insurance对象是否为null,之后再调用它的getName方法,如下所示:
Insurance insurance = ...;if(insurance != null && "CambridgeInsurance".equals(insurance.getName())){System.out.println("ok");}
使用Optional对象的filter方法,这段代码可以重构如下:
Optional<Insurance> optInsurance = ...;optInsurance.filter(insurance ->"CambridgeInsurance".equals(insurance.getName())).ifPresent(x -> System.out.println("ok"));
filter方法接受一个谓词作为参数。如果Optional对象的值存在,并且它符合谓词的条件,filter方法就返回其值;否则它就返回一个空的Optional对象。如果你还记得我们可以将Optional看成最多包含一个元素的Stream对象,这个方法的行为就非常清晰了。如果Optional对象为空,那它不做任何操作,反之,它就对Optional对象中包含的值施加谓词操作。如果该操作的结果为true,那它不做任何改变,直接返回该Optional对象,否则就将该值过滤掉,将Optional的值置空。通过测验11.2,可以测试你对filter方法工作方式的理解。
测验11.2:对
Optional对象进行过滤假设在我们的
Person/Car/Insurance模型中,Person还提供了一个方法getAge可以取得Person对象的年龄,请使用下面的签名改写代码清单11-5中的getCarInsuranceName方法:
public String getCarInsuranceName(Optional<Person> person, int minAge)找出年龄大于或者等于
minAge参数的Person所对应的保险公司列表。答案:你可以对
Optional封装的Person对象进行filter操作,设置相应的条件谓词,即如果person的年龄大于minAge参数的设定值,就返回该值,并将谓词传递给filter方法,代码如下所示:
public String getCarInsuranceName(Optional<Person> person, int minAge) {return person.filter(p -> p.getAge() >= minAge).flatMap(Person::getCar).flatMap(Car::getInsurance).map(Insurance::getName).orElse("Unknown");}
下一节中,我们会探讨Optional类剩下的一些特性,并提供更实际的例子,展示多种你能够应用于代码中更好地管理缺失值的技巧。
表11-1对Optional类中的方法进行了分类和概括。
表 11-1 Optional类的方法
| 方法 | 描述 |
|---|---|
empty
|
返回一个空的Optional实例
|
filter
|
如果值存在并且满足提供的谓词,就返回包含该值的Optional对象;否则返回一个空的Optional对象
|
flatMap
|
如果值存在,就对该值执行提供的mapping函数调用,返回一个Optional类型的值,否则就返回一个空的Optional对象
|
get
|
如果值存在,就将该值用Optional封装返回,否则抛出一个NoSuchElementException异常
|
ifPresent
| 如果值存在,就执行使用该值的方法调用,否则什么也不做 |
ifPresentOrElse
| 如果值存在,就以值作为输入执行对应的方法调用,否则执行另一个不需任何输入的方法 |
isPresent
|
如果值存在就返回true,否则返回false
|
map
|
如果值存在,就对该值执行提供的mapping函数调用
|
of
|
将指定值用Optional封装之后返回,如果该值为null,则抛出一个NullPointerException异常
|
ofNullable
|
将指定值用Optional封装之后返回,如果该值为null,则返回一个空的Optional对象
|
or
|
如果值存在,就返回同一个Optional对象,否则返回由支持函数生成的另一个Optional对象
|
orElse
| 如果有值则将其返回,否则返回一个默认值 |
orElseGet
|
如果有值则将其返回,否则返回一个由指定的Supplier接口生成的值
|
orElseThrow
|
如果有值则将其返回,否则抛出一个由指定的Supplier接口生成的异常
|
stream
|
如果有值,就返回包含该值的一个Stream,否则返回一个空的Stream
|
