JAVA高级(二)——Optional


一、概述


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​​就像是一个容器,里面放一个泛型,


  1. 如果泛型对象为空,那么这个​​Optional<T>​​​ 就是 ​​null​​;
  2. 否者,可以调用​​Optional​​​ 中的方法进行操作里面的​​对象​​​元素;(接下里会具体介绍​​Optional​​中的方法)

JAVA高级(二)——Optional_jdk


变量存在时,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封装的值呢?


  1. 声明一个空的​​Optional​​正如前文所述,你可以通过静态工厂方法​​Optional.empty​​创建一个空的​​Optional​​对象:

JAVA高级(二)——Optional_java_02

Optional<Car> optCar = Optional.empty();
  1. 依据一个非空值创建Optional
    你还可以使用静态工厂方法Optional.of依据一个非空值创建一个Optional对象:

JAVA高级(二)——Optional_静态工厂方法_03

Optional<Car> optCar = Optional.of(car);
  1. 可接受null的Optional
    最后,使用静态工厂方法​​Optional.ofNullable​​,你可以创建一个允许​​null​​值的​​Optional​​对象:
    JAVA高级(二)——Optional_java_04
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方法对比

JAVA高级(二)——Optional_jdk_05

但是如何重构下面代码呢?

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​​就出现了。

JAVA高级(二)——Optional_数据_06

???? 使用两层的​​Optional​​对象


​flatMap​​​方法。使用流时,​​flatMap​​​方法接受一个函数作为参数,这个函数的返回值是另一个流。这个方法会应用到流中的每一个元素,最终形成一个新的流的流。但是​​flagMap​​会用流的内容替换每个新生成的流。换句话说,由方法生成的各个流会被合并或者扁平化为一个单一的流。这里你希望的结果其实也是类似的,但是你想要的是将两层的​​Optional​​合并为一个。


JAVA高级(二)——Optional_jdk_07

​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​​公司名称的字符串。下图进行了说明:

JAVA高级(二)——Optional_jdk_08

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());
}

例子讲解:


  1. 将​​persons​​​ 转换为 ​​stream​​​-> ​​Stream<Person>​​;
  2. 通过第一个 ​​map​​​ 将数据转换为:​​Optional<Stream<Car>>​​;
  3. 第二个​​map​​​对每个​​Optional<Car>​​​执行​​flatMap​​​操作,将其转换成对应的​​Optional<Insurance>​​对象
  4. 第三个​​map​​​将每一个 ​​Optional<Insurance>​​​ 执行 ​​flatMap​​​操作将 ​​Optional<Insurance>​​​转换为​​Optional<String>​​;
  5. 使用 ​​flatMap​​​ 将​​Stream<Optional<String>>​​​转换为​​Stream<String>​​对象,只保留流中那些存在保险公司名的对象;
  6. 收集成为​​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)));
}

执行流程:


  1. ​flatMap​​​判断​​person​​是否为空,如果为空就不执行;
  2. 如果 ​​person​​​存在,这次调用就会将其作为一个​​Function​​​进行传入,并按照与flatMap方法的约定返回​​Optional<Insurance>​​对象 ;
  3. 这个函数的函数体会对第二个​​Optional​​​对象执行​​map​​​操作,如果第二个对象不包含​​car​​​,函数​​Function​​​就返回一个空的​​Optional​​​对象,整个​​nullSafeFindCheapestInsurance​​​方法的返回值也是一个空的​​Optional​​对象。
  4. 最后,如果​​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类的方法

JAVA高级(二)——Optional_数据_09

JAVA高级(二)——Optional_jdk_10

四、小结



  • null引用在历史上被引入到程序设计语言中,目的是为了表示变量值的缺失。
  • Java 8中引入了一个新的类java.util.Optional,对存在缺失的变量值进行建模
  • 你可以使用静态工厂方法Optional.empty、Optional.of以及Optional.ofNullable创建Optional对象。
  • Optional类支持多种方法,比如mapflatMapfilter,它们在概念上与Stream类中对应的方法十分相似。
  • 使用Optional会迫使你更积极地解引用Optional对象,以应对变量值缺失的问题,最终,你能更有效地防止代码中出现不期而至的空指针异常。