JAVA高级——Optional

发布于:2023-02-03 ⋅ 阅读:(446) ⋅ 点赞:(0)

JAVA高级——Optional

    • 二、为何要避免null指针
    • 三、使用Optional优化null判断
        • 3.1.1 使用Optional优化Car类
        • 3.1.2 Optional的几种模式
        • 3.1.3 使用map从Optional中提取值
        • 3.1.4 使用flatMap链接Optional对象
          • 3.1.4.1 使用Optional获取car的保险公司名称
          • 3.1.4.2 使用Optional解引用串接的Person/Car/Insurance对象
        • 3.1.5 操作由Optional对象构成的Stream流
        • 3.1.6 默认行为及解引用Optional对象
        • 3.1.7 两个Optional对象的组合
        • 3.1.8 使用Filter进行剔除

一、概述

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

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

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

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

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

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

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

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

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

 使用两层的 Optional 对象

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

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

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

四、小结

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

网站公告

今日签到

点亮在社区的每一天
去签到