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中的方法)

image-20211117111728688

变量存在时,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对象:

image-20211117114713865

Optional<Car> optCar = Optional.empty();
  1. 依据一个非空值创建Optional

    你还可以使用静态工厂方法Optional.of依据一个非空值创建一个Optional对象:

image-20211117114837513

Optional<Car> optCar = Optional.of(car);		
  1. 可接受null的Optional

    最后,使用静态工厂方法Optional.ofNullable,你可以创建一个允许null值的Optional对象:

    image-20211117123251430

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方法对比

image-20211117123932569

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

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

image-20211117124727212

? 使用两层的Optional对象

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

image-20211117125420985

StreamOptionalFlatMap 对比

如上图可以看出,

  • 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>对象,我们可以结合使用之前介绍的mapflatMap方法,从Person中解引用出Car,从Car中解引用出Insurance,从Insurance对象中解引用出包含insurance公司名称的字符串。下图进行了说明:

image-20211117130038440

3.1.5 操作由Optional对象构成的Stream流

Java 9引入了Optionalstream()方法,使用该方法可以把一个含值的Optional对象转换成由该值构成的Stream对象,或者把一个空的Optional对象转换成等价的空Stream。这一技术为典型流处理场景带来了极大的便利:当你要处理的对象是由Optional对象构成的Stream时,你需要将这个Stream转换为由原Stream中非空Optional对象值组成的新Stream。本节会通过一个实际例子演示为什么你需要处理由Optional对象构成的Stream,以及如何执行这种操作。

接下来一个例子说明 OptionalStream流怎么用:

? 业务场景:找出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. 使用 flatMapStream<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. 最后,如果personcar对象都存在,那么作为参数传递给map方法的Lambda表达式就能够使用这两个值安全地调用原始的findCheapestInsurance方法,完成期望的操作。

3.1.8 使用Filter进行剔除

例如:我们检查公司名字是否为“xiao company”。为了以一种安全的方式进行操作,所以我们可以需要判断这个名字是否为null,代码如下

Insurance insurance = ...;
if(insurance != null && "xiao company".equals(insurance.getName())){
  System.out.println("ok");
}

使用Optionalfilter进行重构代码:

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类的方法

image-20211117185535246

image-20211117185552109

四、小结

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

版权声明:本文为weixin_44954070原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。