文章目录
- 在Optional出现之前经常遇到的空指针异常
- Optional的介绍以及API的详解
- Optional类的方法
在Optional出现之前经常遇到的空指针异常
NullPointerException 这个你总不能说你没有碰到过吧
【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 ,结果又会怎样?
采用防御式减少NullPointerException (深度质疑)
怎样做才能避免这种不期而至的 NullPointerException 呢?通常,你可以在需要的地方添加 null 的检查(过于激进的防御式检查甚至会在不太需要的地方添加检测代码),并且添加的方式往往各有不同。
下面这个例子是我们试图在方法中避免 NullPointerException 的第一次尝试
这个方法每次引用一个变量都会做一次 null 检查,如果引用链上的任何一个遍历的解变量
值为 null ,它就返回一个值为“Unknown”的字符串。
每次你不确定一个变量是否为 null 时,都需要添加一个进一步嵌套的 if 块,也增加了代码缩进的层数。很明显,这种方式不具备扩展性,同时还降低了代码的可读性。
面对这种情况,你也许愿意尝试另一种方案。下面的代码清单中,我们试图通过一种不同的方式避免这种问题。
null-安全的第二种尝试(过多的退出语句)
为了避免深层递归的 if 语句块,采用了一种不同的策略: 每次遇到null, 都返回一个unknown常量。
然而,这种方案远非理想,现在这个方法有了四个不同的退出点,使得代码的维护异常困难。
更糟糕的是,发生 null 时返回的默认值,即字符串“Unknown”在三个不同的地方重复出现——出现拼写写错误的概率不小!当然,你可能会说,我们可以用把它们抽取到一个常量中的方式避免这种问题。
进一步而言,这种流程是极易出错的;如果你忘记检查了那个可能为 null 的属性会怎样?
使用 null 来表示变量值的缺失是大错特错的。你需要更优雅的方式来对缺失的变量值建模。
Optional的介绍以及API的详解
Java 8中引入了一个新的类 java.util.Optional<T>
。这是一个封装 Optional 值的类。
举例来说,使用新的类意味着,如果你知道一个人可能有也可能没有车,那么 Person 类内部的 car 变量就不应该声明为 Car ,遭遇某人没有车时把 null 引用赋值给它,而是应该像下图那样直接将其声明为 Optional<Car>
类型。
变量存在时, Optional 类只是对类简单封装。变量不存在时,缺失的值会被建模成一个“空”的 Optional 对象,由方法 Optional.empty()
返回
Optional.empty()
方法是一个静态工厂,方法,它返回 Optional 类的特定单一实例。
null VS Optional.empty
引 用 一 个 null , 一 定 会 触 发 NullPointerException , 不 过 使 用Optional.empty()
就完全没事儿,它是 Optional 类的一个有效对象,多种场景都能调用,非常有用.
使用 Optional 而不是 null 的一个非常重要而又实际的语义区别是,第一个例子中,我们在声明变量时使用的是 Optional<Car>
类型,而不是 Car 类型,这句声明非常清楚地表明了这里发生变量缺失是允许的。
public class Person {
private Optional<Car> car;
public Optional<Car> getCar() {
return this.car;
}
}
public class Car {
private Optional<Insurance> insurance;
public Optional<Insurance> getInsurance() {
return insurance;
}
}
与此相反,使用 Car 这样的类型,可能将变量赋值为 null ,这意味着你需要独立面对这些,你只能依赖你对业务模型的理解,判断一个 null 是否属于该变量的有效范围。
使用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 中解包出来,直面缺失的变量值。
如何使用 Optional
创建Optional对象
使用 Optional 之前,你首先需要学习的是如何创建 Optional 对象。完成这一任务有多种方法。
1.声明一个空的Optional
可以通过静态工厂方法 Optional.empty ,创建一个空的 Optional对象
Optional<Car> car = Optional.empty();
2.使用一个非空创建Optional
还可以使用静态工厂方法 Optional.of ,依据一个非空值创建一个 Optional 对象:
Car car1 = new Car();
Optional<Car> o = Optional.of(car1);
如果 car 是一个 null ,这段代码会立即?出一个 NullPointerException ,而不是等到你试图访问 car 的属性值时才返回一个错误。
Optional<Car> o = Optional.of(null);
3.可接受null值的Optional
最后,使用静态工厂方法 Optional.ofNullable ,你可以创建一个允许 null 值的 Optional对象
Car car1 = new Car();
Optional<Car> o = Optional.ofNullable(car1);
System.out.println(o);
Optional<Car> o2 = Optional.ofNullable(null);
System.out.println(o2);
如果 car 是 null ,那么得到的 Optional 对象就是个空对象。
创建完成了, 我们还需要继续研究“如何获取 Optional 变量中的值”。尤其是, Optional
提供了一个 get 方法,它能非常精准地完成这项工作,我们在后面会详细介绍这部分内容。
不过get 方法在遇到空的 Optional 对象时也会抛出异常,所以不按照约定的方式使用它,又会让我们再度陷入由 null 引起的代码维护的梦魇。因此,我们首先从无需显式检查的 Optional 值的使用入手,这些方法与 Stream 中的某些操作极其相似。
使用map从Optional对象中提取和转换值
从对象中提取信息是一种比较常见的模式。比如,你可能想要从 insurance 对象中提取名称。提取名称之前,你需要检查 insurance 对象是否为 null 。
代码如下所示
String name = null;
if(insurance != null){
name = insurance.getName();
}
为了支持这种模式, Optional 提供了一个 map 方法。它的工作方式如下
Optional<Insurance> optionalInsurance = Optional.ofNullable(insurance);
optionalInsurance.map(Insurance::getName);
从概念上,这与我们之前看到的流的 map 方法相差无几。 map 操作会将提供的函数应用于流的每个元素。
你可以把 Optional 对象看成一种特殊的集合数据,它至多包含一个元素。如果 Optional 包含一个值,那函数就将该值作为参数传递给 map ,对该值进行转换。如果 Optional 为空,就什么也不做。
下图对这种相似性进行了说明,展示了把一个将正方形转换为三角形的函数,分别传递给正方形和 Optional 正方形流的 map 方法之后的结果。
这看起来挺有用,但是你怎样才能应用起来,重构之前的代码呢?前文的代码里用安全的方式链接了多个方法。
public String getCarInsuranceName(Person person) {
return person.getCar().getInsurance().getName();
}
为了达到这个目的,我们需要求助 Optional 提供的另一个方法 flatMap
使用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>
类型的对象 ,
这意味着 map 操作的结果是一个 Optional<Optional<Car>>
类型的对象。
因此,它对 getInsurance 的调用是非法的,因为最外层的 optional 对象包含了另一个 optional对象的值,而它当然不会支持 getInsurance 方法。
那遭遇到了的嵌套式 optional结构,该如何解决这个问题呢
让我们再回顾一下在流上使用过的模式:
flatMap 方法。使用流时, flatMap 方法接受一个函数作为参数,这个函数的返回值是另一个流。这个方法会应用到流中的每一个元素,最终形成一个新的流的流。
但是 flagMap 会用流的内容替换每个新生成的流。换句话说,由方法生成的各个流会被合并或者扁平化为一个单一的流
这里你希望的结果其实也是类似的,但是你想要的是将两层的 optional 合并为一个。
这个例子中,传递给流的 flatMap 方法会将每个正方形转换为另一个流中的两个三角形。那么, map 操作的结果就包含有三个新的流,每一个流包含两个三角形,但 flatMap 方法会将这种两层的流合并为一个包含6个三角形的单一流。
类似地,传递给 optional 的 flatMap 方法的函数会将原始包含正方形的 optional 对象转换为包含三角形的 optional 对象。如果将该方法传递给 map 方法,结果会是一个 Optional 对象,而这个 Optional 对象中包含了三角形;但 flatMap方法会将这种两层的 Optional 对象转换为包含三角形的单一 Optional 对象。
使用 Optional 获取car的保险名
我们来比较一下之前的写法
可以看到,处理潜在可能缺失的值时,使用 Optional 具有明显的优势。这一次,你可以用非常容易却又普适的方法实现之前你期望的效果——不再需要使用那么多的条件分支,也不会增加代码的复杂性
从具体的代码实现来看,首先我们注意到修改了代码getCarInsuranceName 方法的签名,因为我们很明确地知道存在这样的用例,即一个不存在的Person 被传递给了方法,比如, Person 是使用某个标识符从数据库中查询出来的,你想要对数库中不存在指定标识符对应的用户数据的情况进行建模。你可以将方法的参数类型由 Person改为 Optional<Person>
,对这种特殊情况进行建模。
我们再一次看到这种方式的优点,它通过类型系统让你的域模型中隐藏的知识显式地体现在你的代码中,换句话说,你永远都不应该忘记语言的首要功能就是沟通,即使对程序设计语言而言也没有什么不同。
声明方法接受一个 Optional 参数,或者将结果作为 Optional 类型返回,让你的同事或者未来你方法的使用者,很清楚地知道它可以接受空值,或者它可能返回一个空值
使用 Optional 解引用串接的 Person / Car / Insurance 对象
由 Optional<Person>
对象,我们可以结合使用之前介绍的 map 和 flatMap 方法,从 Person中解引用出 Car ,从 Car 中解引用出 Insurance ,从 Insurance 对象中解引用出包含 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>
,它可能包含保险的名称,也可能为空。
上面,我们使用了一个名为 orElse 的方法,当 Optional 的值为空时,它会为其设定一个默认值。除此之外,还有很多其他的方法可以为 Optional 设定默认值,或者解析出 Optional 代表的值。
接下来我们一起看看吧。
默认行为及解引用Optional对象
orElse 方法读取这个变量的值,使用这种方式你还可以定义一个默认值,遇空的 Optional 变量时,默认值会作为该方法的调用返回值。
Optional 类提供了多种方法读取Optional 实例中的变量值
get()
get() 是这些方法中最简单但又最不安全的方法。如果变量存在,它直接返回封装的变量
值,否则就抛出一个 NoSuchElementException 异常。
所以,除非你非常确定 Optional变量一定包含值,否则使用这个方法是个相当糟糕的主意。此外,这种方式即便相对于嵌套式的 null检查,也并未体现出多大的改进。
Optional<Person> optionalPerson = Optional.ofNullable(null);
System.out.println(optionalPerson.get());
orElse(T other)
它允许你在Optional 对象不包含值时提供一个默认值.
Optional<String> optional = Optional.ofNullable("artisan");
Optional<String> optional2 = Optional.ofNullable(null);
System.out.println(optional.orElse("default value"));
System.out.println(optional2.orElse("default value"));
orElseGet(Supplier<? extends T> other)
是 orElse 方法的延迟调用版, Supplier方法只有在 Optional 对象不含值时才执行调用。如果创建默认值是件费时费力的工作你应该考虑采用这种方式(借此提升程序的性能),或者你需要非常确定某个方法仅在Optional 为空时才进行调用,也可以考虑该方式(这种情况有严格的限制条件)。
Optional<Person> optionalPerson = Optional.ofNullable(null);
//如果为空,则执行后面的逻辑
System.out.println(optionalPerson.orElseGet(Person::new));
System.out.println(optionalPerson.orElseGet(() -> new Person()));
orElseThrow(Supplier<? extends X> exceptionSupplier)
和 get 方法非常类似,它们遇 Optional 对象为空时都会抛出一个异常,但是使用 orElseThrow 你可以定制希望抛出的异常类型
Optional<Person> optionalPerson = Optional.ofNullable(null);
//如果为空,则执行后面的逻辑 抛出异常
System.out.println(optionalPerson.orElseThrow(RuntimeException::new));
Optional<Person> optionalPerson2 = Optional.ofNullable(null);
//如果为空,则执行后面的逻辑 抛出自定义异常
System.out.println(optionalPerson2.orElseThrow(()->new RuntimeException("null 啦。。。。")));
ifPresent(Consumer<? super T>)
让你能在变量值存在时执行一个作为参数传入的方法,否则就不进行任何操作.
Optional<String> stringOptional = Optional.ofNullable("artisan");
// 如果存在 这执行打印
stringOptional.ifPresent(System.out::println);
Optional<String> stringOptional2 = Optional.ofNullable(null);
// 如果存在 这执行打印
stringOptional2.ifPresent(System.out::println);
可以看到第二个并没有输出,因为 为null ,不满足条件
两个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 类提供的特性,有没有更好或更地道的方式来实现这个方法呢?
【以不解包的方式组合两个 Optional 对象】
结合 map 和 flatMap 方法,用一行代码重新实现之前出现的 nullSafeFind-CheapestInsurance()
方法。 你可以像使用三元操作符元那样,无需任何判断条件的的结构,以一行代码实现该方法
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 方法,完成期望的操作
使用filter删除特定的值
Optional 类和 Stream 接口的相似之处,远不止 map 和 flatMap 这两个方法。还有第三个方法 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 的值置空。
【对Optional 对象进行过滤】
假设在我们的 Person / Car / Insurance 模型中, Person 还提供了一个方法可以取得Person 对象的年龄。
请使用下面的签名改写上面流程中的 getCarInsuranceName 方法:
public String getCarInsuranceName(Optional<Person> person, int minAge)
找出年龄大于或者等于 minAge 参数的 Person 所对应的保险公司列表
分析: 你可以对 Optional 封装的 Person 对象进行 filter 操作, 设置相应的条件谓词,
即如果 person 的年龄大于 minAge 参数的设定值值,就返回该值,并将谓词传递给 filter 方法
Optional类的方法