本章内容
null
引用引发的问题,以及为什么要避免null
引用从
null
到Optional
:以null
安全的方式重写你的域模型让
Optional
发光发热: 去除代码中对null
的检查读取
Optional
中可能值的几种方法对可能缺失值的再思考
如果你作为Java程序员曾经遭遇过NullPointerException
,请举起手。如果这是你最常遭遇的异常,请继续举手。非常可惜,这个时刻,我们无法看到对方,但是我相信很多人的手这个时刻是举着的。我们还猜想你可能也有这样的想法:“毫无疑问,我承认,对任何一位Java程序员来说,无论是初出茅庐的新人,还是久经江湖的专家,NullPointerException
都是他心中的痛,可是我们又无能为力,因为这就是我们为了使用方便甚至不可避免的像null
引用这样的构造所付出的代价。”这就是程序设计世界里大家都持有的观点,然而,这可能并非事实的全部真相,只是我们根深蒂固的一种偏见。
1965年,英国一位名为Tony Hoare的计算机科学家在设计ALGOL W语言时提出了null
引用的想法。ALGOL W是第一批在堆上分配记录的类型语言之一。Hoare选择null
引用这种方式,“只是因为这种方法实现起来非常容易”。虽然他的设计初衷就是要“通过编译器的自动检测机制,确保所有使用引用的地方都是绝对安全的”,他还是决定为null
引用开个绿灯,因为他认为这是为“不存在的值”建模最容易的方式。很多年后,他开始为自己曾经做过这样的决定而后悔不迭,把它称为“我价值百万的重大失误”。我们已经看到它带来的后果——程序员对对象的字段进行检查,判断它的值是否为期望的格式,最终却发现我们查看的并不是一个对象,而是一个空指针,它会立即抛出一个让人厌烦的NullPointerException
异常。
实际上,Hoare的这段话低估了过去五十年来数百万程序员为修复空引用所耗费的代价。近十年出现的大多数现代程序设计语言1,包括Java,都采用了同样的设计方式,其原因是为了与更老的语言保持兼容,或者就像Hoare曾经陈述的那样,“仅仅是因为这样实现起来更加容易”。让我们从一个简单的例子入手,看看使用null
都有什么样的问题。
1为数不多的几个最著名的例外是典型的函数式语言,比如Haskell、ML;这些语言中引入了代数数据类型,允许显式地声明数据类型,明确地定义了特殊变量值(比如null
)能否使用在定义类型的类型(type-by-type basis)中。
10.1 如何为缺失的值建模
假设你需要处理下面这样的嵌套对象,这是一个拥有汽车及汽车保险的客户。
代码清单10-1 Person/Car/Insurance
的数据模型
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;}
这段代码看起来相当正常,但是现实生活中很多人没有车。所以调用getCar
方法的结果会怎样呢?在实践中,一种比较常见的做法是返回一个null
引用,表示该值的缺失,即用户没有车。而接下来,对getInsurance
的调用会返回null
引用的insurance
,这会导致运行时出现一个NullPointerException
,终止程序的运行。但这还不是全部。如果返回的person
值为null
会怎样?如果getInsurance
的返回值也是null
,结果又会怎样?
10.1.1 采用防御式检查减少NullPointerException
怎样做才能避免这种不期而至的NullPointerException
呢?通常,你可以在需要的地方添加null
的检查(过于激进的防御式检查甚至会在不太需要的地方添加检测代码),并且添加的方式往往各有不同。下面这个例子是我们试图在方法中避免NullPointerException
的第一次尝试。
代码清单10-2 null
-安全的第一种尝试:深层质疑
这个方法每次引用一个变量都会做一次null
检查,如果引用链上的任何一个遍历的解变量值为null
,它就返回一个值为“Unknown”的字符串。唯一的例外是保险公司的名字,你不需要对它进行检查,原因很简单,因为任何一家公司必定有个名字。注意到了吗,由于你掌握业务领域的知识,避免了最后这个检查,但这并不会直接反映在你建模数据的Java类之中。
我们将代码清单10-2标记为“深层质疑”,原因是它不断重复着一种模式:每次你不确定一个变量是否为null
时,都需要添加一个进一步嵌套的if
块,也增加了代码缩进的层数。很明显,这种方式不具备扩展性,同时还牺牲了代码的可读性。面对这种窘境,你也许愿意尝试另一种方案。下面的代码清单中,我们试图通过一种不同的方式避免这种问题。
代码清单10-3 null
-安全的第二种尝试:过多的退出语句
第二种尝试中,你试图避免深层递归的if
语句块,采用了一种不同的策略:每次你遭遇null
变量,都返回一个字符串常量“Unknown”。然而,这种方案远非理想,现在这个方法有了四个截然不同的退出点,使得代码的维护异常艰难。更糟的是,发生null
时返回的默认值,即字符串“Unknown”在三个不同的地方重复出现——出现拼写错误的概率不小!当然,你可能会说,我们可以用把它们抽取到一个常量中的方式避免这种问题。
进一步而言,这种流程是极易出错的;如果你忘记检查了那个可能为null
的属性会怎样?通过这一章的学习,你会了解使用null
来表示变量值的缺失是大错特错的。你需要更优雅的方式来对缺失的变量值建模。
10.1.2 null
带来的种种问题
让我们一起回顾一下到目前为止进行的讨论,在Java程序开发中使用null
会带来理论和实际操作上的种种问题。
它是错误之源。
NullPointerException
是目前Java程序开发中最典型的异常。它会使你的代码膨胀。
它让你的代码充斥着深度嵌套的
null
检查,代码的可读性糟糕透顶。它自身是毫无意义的。
null
自身没有任何的语义,尤其是,它代表的是在静态类型语言中以一种错误的方式对缺失变量值的建模。它破坏了Java的哲学。
Java一直试图避免让程序员意识到指针的存在,唯一的例外是:
null
指针。它在Java的类型系统上开了个口子。
null
并不属于任何类型,这意味着它可以被赋值给任意引用类型的变量。这会导致问题,原因是当这个变量被传递到系统中的另一个部分后,你将无法获知这个null
变量最初的赋值到底是什么类型。
为了解业界针对这个问题给出的解决方案,我们一起简单看看其他语言提供了哪些功能。
10.1.3 其他语言中null
的替代品
近年来出现的语言,比如Groovy,通过引入安全导航操作符(Safe Navigation Operator,标记为?
)可以安全访问可能为null
的变量。为了理解它是如何工作的,让我们看看下面这段Groovy代码,它的功能是获取某个用户替他的车保险的保险公司的名称:
def carInsuranceName = person?.car?.insurance?.name
这段代码的表述相当清晰。person
对象可能没有car
对象,你试图通过赋一个null
给Person
对象的car
引用,对这种可能性建模。类似地,car
也可能没有insurance
。Groovy的安全导航操作符能够避免在访问这些可能为null
引用的变量时抛出NullPointerException
,在调用链中的变量遭遇null
时将null
引用沿着调用链传递下去,返回一个null
。
关于Java 7的讨论中曾经建议过一个类似的功能,不过后来又被舍弃了。不知道为什么,我们在Java中似乎并不特别期待出现一种安全导航操作符,几乎所有的Java程序员碰到NullPointerException
时的第一冲动就是添加一个if
语句,在调用方法使用该变量之前检查它的值是否为null
,快速地搞定问题。如果你按照这种方式解决问题,丝毫不考虑你的算法或者你的数据模型在这种状况下是否应该返回一个null
,那么你其实并没有真正解决这个问题,只是暂时地掩盖了问题,使得下次该问题的调查和修复更加困难,而你很可能就是下个星期或下个月要面对这个问题的人。刚才的那种方式实际上是掩耳盗铃,只是在清扫地毯下的灰尘。而Groovy的null
安全解引用操作符也只是一个更强大的扫把,让我们可以毫无顾忌地犯错。你不会忘记做这样的检查,因为类型系统会强制你进行这样的操作。
另一些函数式语言,比如Haskell、Scala,试图从另一个角度处理这个问题。Haskell中包含了一个Maybe
类型,它本质上是对optional
值的封装。Maybe
类型的变量可以是指定类型的值,也可以什么都不是。但是它并没有null
引用的概念。Scala有类似的数据结构,名字叫Option[T]
,它既可以包含类型为T
的变量,也可以不包含该变量,我们在第15章会详细讨论这种类型。要使用这种类型,你必须显式地调用Option
类型的available
操作,检查该变量是否有值,而这其实也是一种变相的“null
检查”。
好了,我们似乎有些跑题了,刚才这些听起来都十分抽象。你可能会疑惑:“那么Java 8提供了什么呢?”嗯,实际上Java 8从“optional
值”的想法中吸取了灵感,引入了一个名为java.util.Optional<T>
的新的类。这一章里,我们会展示使用这种方式对可能缺失的值建模,而不是直接将null
赋值给变量所带来的好处。我们还会阐释从null
到Optional
的迁移,你需要反思的是:如何在你的域模型中使用optional
值。最后,我们会介绍新的Optional
类提供的功能,并附几个实际的例子,展示如何有效地使用这些特性。最终,你会学会如何设计更好的API——用户只需要阅读方法签名就能知道它是否接受一个optional
的值。
10.2 Optional
类入门
汲取Haskell
和Scala
的灵感,Java 8中引入了一个新的类java.util.Optional<T>
。这是一个封装Optional
值的类。举例来说,使用新的类意味着,如果你知道一个人可能有也可能没有车,那么Person
类内部的car
变量就不应该声明为Car
,遭遇某人没有车时把null
引用赋值给它,而是应该像图10-1那样直接将其声明为Optional<Car>
类型。
图 10-1 使用Optional
定义的Car
类
变量存在时,Optional
类只是对类简单封装。变量不存在时,缺失的值会被建模成一个“空”的Optional
对象,由方法Optional.empty
返回。Optional.empty
方法是一个静态工厂方法,它返回Optional
类的特定单一实例。你可能还有疑惑,null
引用和Optional.empty
有什么本质的区别吗?从语义上,你可以把它们当作一回事儿,但是实际中它们之间的差别非常大:如果你尝试解引用一个null
,一定会触发NullPointerException
,不过使用Optional.empty
就完全没事儿,它是Optional
类的一个有效对象,多种场景都能调用,非常有用。关于这一点,接下来的部分会详细介绍。
使用Optional
而不是null
的一个非常重要而又实际的语义区别是,第一个例子中,我们在声明变量时使用的是Optional<Car>
类型,而不是Car
类型,这句声明非常清楚地表明了这里发生变量缺失是允许的。与此相反,使用Car
这样的类型,可能将变量赋值为null
,这意味着你需要独立面对这些,你只能依赖你对业务模型的理解,判断一个null
是否属于该变量的有效范畴。
牢记上面这些原则,你现在可以使用Optional
类对代码清单10-1中最初的代码进行重构,结果如下。
代码清单10-4 使用Optional
重新定义Person/Car/Insurance
的数据模型
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; }}
发现Optional
是如何丰富你模型的语义了吧。代码中person
引用的是Optional<Car>
,而car
引用的是Optional<Insurance>
,这种方式非常清晰地表达了你的模型中一个person
可能拥有也可能没有car
的情形,同样,car
可能进行了保险,也可能没有保险。
与此同时,我们看到insurance
公司的名称被声明成String
类型,而不是Optional<String>
,这非常清楚地表明声明为insurance
公司的类型必须提供公司名称。使用这种方式,一旦解引用insurance
公司名称时发生NullPointerException
,你就能非常确定地知道出错的原因,不再需要为其添加null
的检查,因为null
的检查只会掩盖问题,并未真正地修复问题。insurance
公司必须有个名字,所以,如果你遇到一个公司没有名称,你需要调查你的数据出了什么问题,而不应该再添加一段代码,将这个问题隐藏。
在你的代码中始终如一地使用Optional
,能非常清晰地界定出变量值的缺失是结构上的问题,还是你算法上的缺陷,抑或是你数据中的问题。另外,我们还想特别强调,引入Optional
类的意图并非要消除每一个null
引用。与此相反,它的目标是帮助你更好地设计出普适的API,让程序员看到方法签名,就能了解它是否接受一个Optional
的值。这种强制会让你更积极地将变量从Optional
中解包出来,直面缺失的变量值。
10.3 应用Optional
的几种模式
到目前为止,一切都很顺利;你已经知道了如何使用Optional
类型来声明你的域模型,也了解了这种方式与直接使用null
引用表示变量值的缺失的优劣。但是,我们该如何使用呢?用这种方式能做什么,或者怎样使用Optional
封装的值呢?
10.3.1 创建Optional
对象
使用Optional
之前,你首先需要学习的是如何创建Optional
对象。完成这一任务有多种方法。
1. 声明一个空的Optional
正如前文已经提到,你可以通过静态工厂方法Optional.empty
,创建一个空的Optional
对象:
Optional<Car> optCar = Optional.empty;
2. 依据一个非空值创建Optional
你还可以使用静态工厂方法Optional.of
,依据一个非空值创建一个Optional
对象:
Optional<Car> optCar = Optional.of(car);
如果car
是一个null
,这段代码会立即抛出一个NullPointerException
,而不是等到你试图访问car
的属性值时才返回一个错误。
3. 可接受null
的Optional
最后,使用静态工厂方法Optional.ofNullable
,你可以创建一个允许null
值的Optional
对象:
Optional<Car> optCar = Optional.ofNullable(car);
如果car
是null
,那么得到的Optional
对象就是个空对象。
你可能已经猜到,我们还需要继续研究“如何获取Optional
变量中的值”。尤其是,Optional
提供了一个get
方法,它能非常精准地完成这项工作,我们在后面会详细介绍这部分内容。不过get
方法在遭遇到空的Optional
对象时也会抛出异常,所以不按照约定的方式使用它,又会让我们再度陷入由null
引起的代码维护的梦魇。因此,我们首先从无需显式检查的Optional
值的使用入手,这些方法与Stream
中的某些操作极其相似。
10.3.2 使用map
从Optional
对象中提取和转换值
从对象中提取信息是一种比较常见的模式。比如,你可能想要从insurance
公司对象中提取公司的名称。提取名称之前,你需要检查insurance
对象是否为null
,代码如下所示:
String name = null;if(insurance != null){ name = insurance.getName;}
为了支持这种模式,Optional
提供了一个map
方法。它的工作方式如下(这里,我们继续借用了代码清单10-4的模式):
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);Optional<String> name = optInsurance.map(Insurance::getName);
从概念上,这与我们在第4章和第5章中看到的流的map
方法相差无几。map
操作会将提供的函数应用于流的每个元素。你可以把Optional
对象看成一种特殊的集合数据,它至多包含一个元素。如果Optional
包含一个值,那函数就将该值作为参数传递给map
,对该值进行转换。如果Optional
为空,就什么也不做。图10-2对这种相似性进行了说明,展示了把一个将正方形转换为三角形的函数,分别传递给正方形和Optional
正方形流的map
方法之后的结果。
图 10-2 Stream
和Optional
的map
方法对比
这看起来挺有用,但是你怎样才能应用起来,重构之前的代码呢?前文的代码里用安全的方式链接了多个方法。
public String getCarInsuranceName(Person person) { return person.getCar.getInsurance.getName;}
为了达到这个目的,我们需要求助Optional
提供的另一个方法flatMap
。
10.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<Person>
类型的变量, 调用map
方法应该没有问题。但getCar
返回的是一个Optional<Car>
类型的对象(如代码清单10-4所示),这意味着map
操作的结果是一个Optional<Optional<Car>>
类型的对象。因此,它对getInsurance
的调用是非法的,因为最外层的optional
对象包含了另一个optional
对象的值,而它当然不会支持getInsurance
方法。图10-3说明了你会遭遇的嵌套式optional
结构。
图 10-3 两层的optional
对象
所以,我们该如何解决这个问题呢?让我们再回顾一下你刚刚在流上使用过的模式: flatMap
方法。使用流时,flatMap
方法接受一个函数作为参数,这个函数的返回值是另一个流。这个方法会应用到流中的每一个元素,最终形成一个新的流的流。但是flagMap
会用流的内容替换每个新生成的流。换句话说,由方法生成的各个流会被合并或者扁平化为一个单一的流。这里你希望的结果其实也是类似的,但是你想要的是将两层的optional
合并为一个。
跟图10-2类似,我们借助图10-4来说明flatMap
方法在Stream
和Optional
类之间的相似性。
图 10-4 Stream
和Optional
的flagMap
方法对比
这个例子中,传递给流的flatMap
方法会将每个正方形转换为另一个流中的两个三角形。那么,map
操作的结果就包含有三个新的流,每一个流包含两个三角形,但flatMap
方法会将这种两层的流合并为一个包含六个三角形的单一流。类似地,传递给optional
的flatMap
方法的函数会将原始包含正方形的optional
对象转换为包含三角形的optional
对象。如果将该方法传递给map
方法,结果会是一个Optional
对象,而这个Optional
对象中包含了三角形;但flatMap
方法会将这种两层的Optional
对象转换为包含三角形的单一Optional
对象。
1. 使用Optional
获取car
的保险公司名称
相信现在你已经对Optional
的map
和flatMap
方法有了一定的了解,让我们看看如何应用。代码清单10-2和代码清单10-3的示例用基于Optional
的数据模式重写之后,如代码清单10-5所示。
代码清单10-5 使用Optional
获取car
的Insurance
名称
public String getCarInsuranceName(Optional<Person> person) { return person.flatMap(Person::getCar) .flatMap(Car::getInsurance) .map(Insurance::getName) .orElse("Unknown"); ←─如果Optional的结果值为空,设置默认值}
通过比较代码清单10-5和之前的两个代码清单,我们可以看到,处理潜在可能缺失的值时,使用Optional
具有明显的优势。这一次,你可以用非常容易却又普适的方法实现之前你期望的效果——不再需要使用那么多的条件分支,也不会增加代码的复杂性。
从具体的代码实现来看,首先我们注意到你修改了代码清单10-2和代码清单10-3中的getCarInsuranceName
方法的签名,因为我们很明确地知道存在这样的用例,即一个不存在的Person
被传递给了方法,比如,Person
是使用某个标识符从数据库中查询出来的,你想要对数据库中不存在指定标识符对应的用户数据的情况进行建模。你可以将方法的参数类型由Person
改为Optional<Person>
,对这种特殊情况进行建模。
我们再一次看到这种方式的优点,它通过类型系统让你的域模型中隐藏的知识显式地体现在你的代码中,换句话说,你永远都不应该忘记语言的首要功能就是沟通,即使对程序设计语言而言也没有什么不同。声明方法接受一个Optional
参数,或者将结果作为Optional
类型返回,让你的同事或者未来你方法的使用者,很清楚地知道它可以接受空值,或者它可能返回一个空值。
2. 使用Optional
解引用串接的Person/Car/Insurance
对象
由Optional<Person>
对象,我们可以结合使用之前介绍的map
和flatMap
方法,从Person
中解引用出Car
,从Car
中解引用出Insurance
,从Insurance
对象中解引用出包含insurance
公司名称的字符串。图10-5对这种流水线式的操作进行了说明。
图 10-5 使用Optional
解引用串接的Person/Car/Insurance
这里,我们从以Optional
封装的Person
入手,对其调用flatMap(Person::getCar)
。如前所述,这种调用逻辑上可以划分为两步。第一步,某个Function
作为参数,被传递给由Optional
封装的Person
对象,对其进行转换。这个场景中,Function
的具体表现是一个方法引用,即对Person
对象的getCar
方法进行调用。由于该方法返回一个Optional<Car>
类型的对象,Optional
内的Person
也被转换成了这种对象的实例,结果就是一个两层的Optional
对象,最终它们会被flagMap
操作合并。从纯理论的角度而言,你可以将这种合并操作简单地看成把两个Optional
对象结合在一起,如果其中有一个对象为空,就构成一个空的Optional
对象。如果你对一个空的Optional
对象调用flatMap
,实际情况又会如何呢?结果不会发生任何改变,返回值也是个空的Optional
对象。与此相反,如果Optional
封装了一个Person
对象,传递给flapMap
的Function
,就会应用到Person
上对其进行处理。这个例子中,由于Function
的返回值已经是一个Optional
对象,flapMap
方法就直接将其返回。
第二步与第一步大同小异,它会将Optional<Car>
转换为Optional<Insurance>
。第三步则会将Optional<Insurance>
转化为Optional<String>
对象,由于Insurance.getName
方法的返回类型为String
,这里就不再需要进行flapMap
操作了。
截至目前为止,返回的Optional
可能是两种情况:如果调用链上的任何一个方法返回一个空的Optional
,那么结果就为空,否则返回的值就是你期望的保险公司的名称。那么,你如何读出这个值呢?毕竟你最后得到的这个对象还是个Optional<String>
,它可能包含保险公司的名称,也可能为空。代码清单10-5中,我们使用了一个名为orElse
的方法,当Optional
的值为空时,它会为其设定一个默认值。除此之外,还有很多其他的方法可以为Optional
设定默认值,或者解析出Optional
代表的值。接下来我们会对此做进一步的探讨。
在域模型中使用
Optional
,以及为什么它们无法序列化在代码清单10-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); }}
10.3.4 默认行为及解引用Optional
对象
我们决定采用orElse
方法读取这个变量的值,使用这种方式你还可以定义一个默认值,遭遇空的Optional
变量时,默认值会作为该方法的调用返回值。Optional
类提供了多种方法读取Optional
实例中的变量值。
get
是这些方法中最简单但又最不安全的方法。如果变量存在,它直接返回封装的变量值,否则就抛出一个NoSuchElementException
异常。所以,除非你非常确定Optional
变量一定包含值,否则使用这个方法是个相当糟糕的主意。此外,这种方式即便相对于嵌套式的null
检查,也并未体现出多大的改进。orElse(T other)
是我们在代码清单10-5中使用的方法,正如之前提到的,它允许你在Optional
对象不包含值时提供一个默认值。orElseGet(Supplier<? extends T> other)
是orElse
方法的延迟调用版,Supplier
方法只有在Optional
对象不含值时才执行调用。如果创建默认值是件耗时费力的工作,你应该考虑采用这种方式(借此提升程序的性能),或者你需要非常确定某个方法仅在Optional
为空时才进行调用,也可以考虑该方式(这种情况有严格的限制条件)。orElseThrow(Supplier<? extends X> exceptionSupplier)
和get
方法非常类似,它们遭遇Optional
对象为空时都会抛出一个异常,但是使用orElseThrow
你可以定制希望抛出的异常类型。ifPresent(Consumer<? super T>)
让你能在变量值存在时执行一个作为参数传入的方法,否则就不进行任何操作。
Optional
类和Stream
接口的相似之处,远不止map
和flatMap
这两个方法。还有第三个方法filter
,它的行为在两种类型之间也极其相似,我们会在10.3.6节做进一步的介绍。
10.3.5 两个Optional
对象的组合
现在,我们假设你有这样一个方法,它接受一个Person
和一个Car
对象,并以此为条件对外部提供的服务进行查询,通过一些复杂的业务逻辑,试图找到满足该组合的最便宜的保险公司:
public Insurance findCheapestInsurance(Person person, Car car) { // 不同的保险公司提供的查询服务 // 对比所有数据 return cheapestCompany;}
我们还假设你想要该方法的一个null
-安全的版本,它接受两个Optional
对象作为参数,返回值是一个Optional<Insurance>
对象,如果传入的任何一个参数值为空,它的返回值亦为空。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
类提供的特性,有没有更好或更地道的方式来实现这个方法呢? 花几分钟时间思考一下测验10.1,试试能不能找到更优雅的解决方案。
测验10.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<Insurance>
对象。这个函数的函数体会对第二个Optional
对象执行map
操作,如果第二个对象不包含car
,函数Function
就返回一个空的Optional
对象,整个nullSafeFindCheapestInsuranc
方法的返回值也是一个空的Optional
对象。最后,如果person
和car
对象都存在,作为参数传递给map
方法的Lambda
表达式能够使用这两个值安全地调用原始的findCheapestInsurance
方法,完成期望的操作。
Optional
类和Stream
接口的相似之处远不止map
和flatMap
这两个方法。还有第三个方法filter
,它的行为在两种类型之间也极其相似,我们在接下来的一节会进行介绍。
10.3.6 使用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
的值置空。通过测验10.2,可以测试你对filter
方法工作方式的理解。
测验10.2:对
Optional
对象进行过滤假设在我们的
Person/Car/Insurance
模型中,Person
还提供了一个方法可以取得Person
对象的年龄,请使用下面的签名改写代码清单10-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
类剩下的一些特性,并提供更实际的例子,展示多种你能够应用于代码中更好地管理缺失值的技巧。
表10-1对Optional
类中的方法进行了分类和概括。
表10-1 Optional
类的方法
方法
描述
empty
返回一个空的Optional
实例
filter
如果值存在并且满足提供的谓词,就返回包含该值的Optional
对象;否则返回一个空的Optional
对象
flatMap
如果值存在,就对该值执行提供的mapping函数调用,返回一个Optional
类型的值,否则就返回一个空的Optional
对象
get
如果该值存在,将该值用Optional
封装返回,否则抛出一个NoSuchElementException
异常
ifPresent
如果值存在,就执行使用该值的方法调用,否则什么也不做
isPresent
如果值存在就返回true
,否则返回false
map
如果值存在,就对该值执行提供的mapping函数调用
of
将指定值用Optional
封装之后返回,如果该值为null
,则抛出一个NullPointerException
异常
ofNullable
将指定值用Optional
封装之后返回,如果该值为null
,则返回一个空的Optional
对象
orElse
如果有值则将其返回,否则返回一个默认值
orElseGet
如果有值则将其返回,否则返回一个由指定的Supplier
接口生成的值
orElseThrow
如果有值则将其返回,否则抛出一个由指定的Supplier
接口生成的异常
10.4 使用Optional
的实战示例
相信你已经了解,有效地使用Optional
类意味着你需要对如何处理潜在缺失值进行全面的反思。这种反思不仅仅限于你曾经写过的代码,更重要的可能是,你如何与原生Java API实现共存共赢。
实际上,我们相信如果Optional
类能够在这些API创建之初就存在的话,很多API的设计编写可能会大有不同。为了保持后向兼容性,我们很难对老的Java API进行改动,让它们也使用Optional
,但这并不表示我们什么也做不了。你可以在自己的代码中添加一些工具方法,修复或者绕过这些问题,让你的代码能享受Optional
带来的威力。我们会通过几个实际的例子讲解如何达到这样的目的。
10.4.1 用Optional
封装可能为null
的值
现存Java API几乎都是通过返回一个null
的方式来表示需要值的缺失,或者由于某些原因计算无法得到该值。比如,如果Map
中不含指定的键对应的值,它的get
方法会返回一个null
。但是,正如我们之前介绍的,大多数情况下,你可能希望这些方法能返回一个Optional
对象。你无法修改这些方法的签名,但是你很容易用Optional
对这些方法的返回值进行封装。我们接着用Map
做例子,假设你有一个Map<String, Object>
方法,访问由key
索引的值时,如果map
中没有与key
关联的值,该次调用就会返回一个null
。
Object value = map.get("key");
使用Optional
封装map
的返回值,你可以对这段代码进行优化。要达到这个目的有两种方式:你可以使用笨拙的if-then-else
判断语句,毫无疑问这种方式会增加代码的复杂度;或者你可以采用我们前文介绍的Optional.ofNullable
方法:
Optional<Object> value = Optional.ofNullable(map.get("key"));
每次你希望安全地对潜在为null
的对象进行转换,将其替换为Optional
对象时,都可以考虑使用这种方法。
10.4.2 异常与Optional
的对比
由于某种原因,函数无法返回某个值,这时除了返回null
,Java API比较常见的替代做法是抛出一个异常。这种情况比较典型的例子是使用静态方法Integer.parseInt(String)
,将String
转换为int
。在这个例子中,如果String
无法解析到对应的整型,该方法就抛出一个NumberFormatException
。最后的效果是,发生String
无法转换为int
时,代码发出一个遭遇非法参数的信号,唯一的不同是,这次你需要使用try/catch
语句,而不是使用if
条件判断来控制一个变量的值是否非空。
你也可以用空的Optional
对象,对遭遇无法转换的String
时返回的非法值进行建模,这时你期望parseInt
的返回值是一个optional
。我们无法修改最初的Java方法,但是这无碍我们进行需要的改进,你可以实现一个工具方法,将这部分逻辑封装于其中,最终返回一个我们希望的Optional
对象,代码如下所示。
代码清单10-6 将String
转换为Integer
,并返回一个Optional
对象
public static Optional<Integer> stringToInt(String s) { try { return Optional.of(Integer.parseInt(s)); ←─如果String能转换为对应的Integer,将其封装在Optioal对象中返回 } catch (NumberFormatException e) { return Optional.empty; ←─否则返回一个空的Optional对象 }}
我们的建议是,你可以将多个类似的方法封装到一个工具类中,让我们称之为OptionalUtility
。通过这种方式,你以后就能直接调用OptionalUtility.stringToInt
方法,将String
转换为一个Optional<Integer>
对象,而不再需要记得你在其中封装了笨拙的try/catch
的逻辑了。
基础类型的Optional
对象,以及为什么应该避免使用它们
不知道你注意到了没有,与Stream
对象一样,Optional
也提供了类似的基础类 型——OptionalInt
、OptionalLong
以及OptionalDouble
——所以代码清单10-6中的方法可以不返回Optional<Integer>
,而是直接返回一个OptionalInt
类型的对象。第5章中,我们讨论过使用基础类型Stream
的场景,尤其是如果Stream
对象包含了大量元素,出于性能的考量,使用基础类型是不错的选择,但对Optional
对象而言,这个理由就不成立了,因为Optional
对象最多只包含一个值。
我们不推荐大家使用基础类型的Optional
,因为基础类型的Optional
不支持map
、flatMap
以及filter
方法,而这些却是Optional
类最有用的方法(正如我们在10.2节所看到的那样)。此外,与Stream
一样,Optional
对象无法由基础类型的Optional
组合构成,所以,举例而言,如果代码清单10-6中返回的是OptionalInt
类型的对象,你就不能将其作为方法引用传递给另一个Optional
对象的flatMap
方法。
10.4.3 把所有内容整合起来
为了展示之前介绍过的Optional
类的各种方法整合在一起的威力,我们假设你需要向你的程序传递一些属性。为了举例以及测试你开发的代码,你创建了一些示例属性,如下所示:
Properties props = new Properties;props.setProperty("a", "5");props.setProperty("b", "true");props.setProperty("c", "-3");
现在,我们假设你的程序需要从这些属性中读取一个值,该值是以秒为单位计量的一段时间。由于一段时间必须是正数,你想要该方法符合下面的签名:
public int readDuration(Properties props, String name)
即,如果给定属性对应的值是一个代表正整数的字符串,就返回该整数值,任何其他的情况都返回0。为了明确这些需求,你可以采用JUnit的断言,将它们形式化:
assertEquals(5, readDuration(param, "a"));assertEquals(0, readDuration(param, "b"));assertEquals(0, readDuration(param, "c"));assertEquals(0, readDuration(param, "d"));
这些断言反映了初始的需求:如果属性是a
,readDuration
方法返回5
,因为该属性对应的字符串能映射到一个正数;对于属性b
,方法的返回值是0
,因为它对应的值不是一个数字;对于c
,方法的返回值是0
,因为虽然它对应的值是个数字,不过它是个负数;对于d
,方法的返回值是0
,因为并不存在该名称对应的属性。让我们以命令式编程的方式实现满足这些需求的方法,代码清单如下所示。
代码清单10-7 以命令式编程的方式从属性中读取duration
值
public int readDuration(Properties props, String name) { String value = props.getProperty(name); if (value != null) { ←─确保名称对应的属性存在 try { int i = Integer.parseInt(value); ←─将String属性转换为数字类型 if (i > 0) { ←─检查返回的数字是否为正数 return i; } } catch (NumberFormatException nfe) { } } return 0; ←─如果前述的条件都不满足,返回0}
你可能已经预见,最终的实现既复杂又不具备可读性,呈现为多个由if
语句及try/catch
块儿构成的嵌套条件。花几分钟时间思考一下测验10.3, 想想怎样使用本章内容实现同样的效果。
测验10.3:使用
Optional
从属性中读取duration
请尝试使用
Optional
类提供的特性及代码清单10-6中提供的工具方法,通过一条精炼的语句重构代码清单10-7中的方法。答案:如果需要访问的属性值不存在,
Properties.getProperty(String)
方法的返回值就是一个null
,使用ofNullable
工厂方法非常轻易地就能把该值转换为Optional
对象。接着,你可以向它的flatMap
方法传递代码清单10-6中实现的OptionalUtility.stringToInt
方法的引用,将Optional<String>
转换为Optional<Integer>
。最后,你非常轻易地就可以过滤掉负数。这种方式下,如果任何一个操作返回一个空的Optional
对象,该方法都会返回orElse
方法设置的默认值0
;否则就返回封装在Optional
对象中的正整数。下面就是这段简化的实现:public int readDuration(Properties props, String name) { return Optional.ofNullable(props.getProperty(name)) .flatMap(OptionalUtility::stringToInt) .filter(i -> i > 0) .orElse(0);}
注意到使用Optional
和Stream
时的那些通用模式了吗?它们都是对数据库查询过程的反思,查询时,多种操作会被串接在一起执行。
10.5 小结
这一章中,你学到了以下的内容。
null
引用在历史上被引入到程序设计语言中,目的是为了表示变量值的缺失。Java 8中引入了一个新的类
java.util.Optional<T>
,对存在或缺失的变量值进行建模。你可以使用静态工厂方法
Optional.empty
、Optional.of
以及Optional.ofNullable
创建Optional
对象。Optional
类支持多种方法,比如map
、flatMap
、filter
,它们在概念上与Stream
类中对应的方法十分相似。使用
Optional
会迫使你更积极地解引用Optional
对象,以应对变量值缺失的问题,最终,你能更有效地防止代码中出现不期而至的空指针异常。使用
Optional
能帮助你设计更好的API,用户只需要阅读方法签名,就能了解该方法是否接受一个Optional
类型的值。