如何为缺失的值建模

  • Java中的空指针异常,随处可见,随处都可能存在。现在看个例子,
  • 汽车及汽车保险的客户
  • Person/Car/Insurance的数据模型 ```java @Data public class Person { private Car car; }
  1. ```java
  2. @Data
  3. public class Car {
  4. private Insurance insurance;
  5. }
  1. @Data
  2. public class Insurance {
  3. private String name;
  4. }
  • 获取汽车保险的名字:有无问题?

    1. public static String getCarInsuranceName(Person person) {
    2. return person.getCar().getInsurance().getName();
    3. }
  • 除非你能保证每个人都有车、每辆车都买保险。否则上面的代码随时抛出NPE

  • 采用防御式检查减少NullPointerException:深层质疑

    1. public static String getCarInsuranceName(Person person) {
    2. if (person != null) {
    3. Car car = person.getCar();
    4. if (car != null) {
    5. Insurance insurance = car.getInsurance();
    6. if (insurance != null) {
    7. return insurance.getName();
    8. }
    9. }
    10. }
    11. return null;
    12. }
  • 是不是想口吐芬芳了~

  • 采用防御式检查减少NullPointerException:过多的退出语句

    1. public static String getCarInsuranceName(Person person) {
    2. if (person == null) {
    3. return "Unknown";
    4. }
    5. Car car = person.getCar();
    6. if (car == null) {
    7. return "Unknown";
    8. }
    9. Insurance insurance = car.getInsurance();
    10. if (insurance == null) {
    11. return "Unknown";
    12. }
    13. return insurance.getName();
    14. }
  • null带来的种种问题

  • 它是错误之源:NullPointerException是目前Java程序开发中最典型的异常
  • 它会使你的代码膨胀:它让你的代码充斥着深度嵌套的null检查,代码的可读性糟糕透顶
  • 它自身是毫无意义的:null自身没有任何的语义,尤其是,它代表的是在静态类型语言中以一种错误的方式对缺失变量值的建模
  • 错误的方式对缺失变量值的建模:Java一直试图避免让程序员意识到指针的存在,唯一的例外是:null指针
  • 它在Java的类型系统上开了个口子:null并不属于任何类型,这意味着它可以被赋值给任意引用类型的变量。这会导致问题,原因是当这个变量被传递到系统中的另一个部分后,你将无法获知这个null变量最初的赋值到底是什么类型

    Optional类入门

  • Java 8中引入了一个新的类java.util.Optional。这是一个封装Optional值的类

  • 如果你知道一个人可能有也可能没有车,那么Person类内部的car变量就不应该声明为Car,遭遇某人没有车时把null引用赋值给它,而是应该直接将其声明为Optional类型。

image.png

  • 变量存在时,Optional类只是对类简单封装。变量不存在时,缺失的值会被建模成一个“空”的Optional对象,由方法Optional.empty()返回。Optional.empty()方法是一个静态工厂方法,它返回Optional类的特定单一实例。你可能还有疑惑,null引用和Optional.empty()有什么本质的区别吗?从语义上,你可以把它们当作一回事儿,但是实际中它们之间的差别非常大:如果你尝试解引用一个null,一定会触发NullPointerException,不过使用Optional.empty()就完全没事儿,它是Optional类的一个有效对象,多种场景都能调用,非常有用。
  • 使用Optional而不是null的一个非常重要而又实际的语义区别是,第一个例子中,我们在声明变量时使用的是Optional类型,而不是Car类型,这句声明非常清楚地表明了这里发生变量缺失是允许的。与此相反,使用Car这样的类型,可能将变量赋值为null,这意味着你需要独立面对这些,你只能依赖你对业务模型的理解,判断一个null是否属于该变量的有效范畴。
  • 重构一手上面的案例 ```java @Data public class Person { private Optional car; }
  1. ```java
  2. @Data
  3. public class Car {
  4. private Optional<Insurance> insurance;
  5. }
  1. @Data
  2. public class Insurance {
  3. private String name;
  4. }

应用Optional的几种模式

  • 创建Optional对象
  • 声明一个空的Optional

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

  • 如果car是一个null,这段代码会立即抛出一个NullPointerException,而不是等到你试图访问car的属性值时才返回一个错误。

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

  • 如果car是null,那么得到的Optional对象就是个空对象。

    1. Optional<Car> carOptional= Optional.ofNullable(car);
  • 使用Optional获取car的保险名称

  • 下面的方法为什么使用flatMap,因为car是Optional类型包装起来的,如果是map,那么则是Optional>.map(xxx) 这样是不正确的,需要扁平化流。 ```java public class Test1 { public static void main(String[] args) {

    1. Optional<Person> personOptional = Optional.empty();
    2. System.out.println(getCarInsuranceName(personOptional));

    }

    public static String getCarInsuranceName(Optional person) {

    1. return person.flatMap(Person::getCar).flatMap(Car::getInsurance).map(Insurance::getName).orElse("Unknown");

    } }

  1. - 从具体的代码实现来看,首先我们注意到修改的getCarInsuranceName方法的签名,因为我们很明确地知道存在这样的用例,即一个不存在的Person被传递给了方法,比如,Person是使用某个标识符从数据库中查询出来的,你想要对数据库中不存在指定标识符对应的用户数据的情况进行建模。你可以将方法的参数类型由Person改为Optional<Person>,对这种特殊情况进行建模。
  2. - 再一次看到这种方式的优点,它通过类型系统让你的域模型中隐藏的知识显式地体现在你的代码中,换句话说,你永远都不应该忘记语言的首要功能就是沟通,即使对程序设计语言而言也没有什么不同。声明方法接受一个Optional参数,或者将结果作为Optional类型返回,让你的同事或者未来你方法的使用者,很清楚地知道它可以接受空值,或者它可能返回一个空值。
  3. - **使用Optional解引用串接的Person/Car/Insurance对象**
  4. - Optional<Person>对象,我们可以结合使用之前介绍的mapflatMap方法,从Person中解引用出Car,从Car中解引用出Insurance,从Insurance对象中解引用出包含insurance公司名称的字符串。
  5. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/1806904/1646560009716-68c82ce4-bb16-4236-97aa-7bd1add94768.png#clientId=ufdd3ccbb-ad66-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=276&id=u7824ea21&margin=%5Bobject%20Object%5D&name=image.png&originHeight=552&originWidth=1590&originalType=binary&ratio=1&rotation=0&showTitle=false&size=61286&status=done&style=none&taskId=uf6a10e14-3924-47ce-90bd-6eed4559413&title=&width=795)
  6. - 这里,我们从以Optional封装的Person入手,对其调用flatMap(Person::getCar)。如前所述,这种调用逻辑上可以划分为两步。第一步,某个Function作为参数,被传递给由Optional封装的Person对象,对其进行转换。这个场景中,Function的具体表现是一个方法引用,即对Person对象的getCar方法进行调用。由于该方法返回一个Optional<Car>类型的对象,Optional内的Person也被转换成了这种对象的实例,结果就是一个两层的Optional对象,最终它们会被flatMap操作合并。从纯理论的角度而言,你可以将这种合并操作简单地看成把两个Optional对象结合在一起,如果其中有一个对象为空,就构成一个空的Optional对象。如果你对一个空的Optional对象调用flatMap,实际情况又会如何呢?结果不会发生任何改变,返回值也是个空的Optional对象。与此相反,如果Optional封装了一个Person对象,传递给flapMapFunction,就会应用到Person上对其进行处理。这个例子中,由于Function的返回值已经是一个Optional对象,flapMap方法就直接将其返回。
  7. - 第二步与第一步大同小异,它会将Optional<Car>转换为Optional<Insurance>。第三步则会将Optional<Insurance>转化为Optional<String>对象,由于Insurance.getName()方法的返回类型为String,这里就不再需要进行flapMap操作了。
  8. - 截至目前为止,返回的Optional可能是两种情况:如果调用链上的任何一个方法返回一个空的Optional,那么结果就为空,否则返回的值就是你期望的保险的名称。那么,你如何读出这个值呢?毕竟你最后得到的这个对象还是个Optional<String>,它可能包含保险的名称,也可能为空。我们使用了一个名为orElse的方法,当Optional的值为空时,它会为其设定一个默认值。
  9. - **在域模型中使用Optional,以及为什么它们无法序列化**
  10. - Optional的设计初衷仅仅是要支持能返回Optional对象的语法。由于Optional类设计时就没特别考虑将其作为类的字段使用,所以它也并未实现Serializable接口。
  11. - **默认行为及解引用Optional对象**
  12. - 采用orElse方法读取这个变量的值,使用这种方式你还可以定义一个默认值,遭遇空的Optional变量时,默认值会作为该方法的调用返回值。Optional类提供了多种方法读取Optional实例中的变量值。
  13. - get()是这些方法中最简单但又最不安全的方法。如果变量存在,它直接返回封装的变量值,否则就抛出一个NoSuchElementException异常。所以,除非你非常确定Optional变量一定包含值,否则使用这个方法是个相当糟糕的主意。此外,这种方式即便相对于嵌套式的null检查,也并未体现出多大的改进。
  14. - orElse(T other)是我们在上面使用的方法,正如之前提到的,它允许你在Optional对象不包含值时提供一个默认值。
  15. - orElseGet(Supplier<? extends T> other)是orElse方法的延迟调用版,Supplier方法只有在Optional对象不含值时才执行调用。如果创建默认值是件耗时费力的工作,你应该考虑采用这种方式(借此提升程序的性能),或者你需要非常确定某个方法仅在Optional为空时才进行调用,也可以考虑该方式(这种情况有严格的限制条件)
  16. - orElseThrow(Supplier<? extends X> exceptionSupplier)和get方法非常类似,它们遭遇Optional对象为空时都会抛出一个异常,但是使用orElseThrow你可以定制希望抛出的异常类型
  17. - ifPresent(Consumer<? super T>)让你能在变量值存在时执行一个作为参数传入的方法,否则就不进行任何操作
  18. - **两个Optional对象的组合**
  19. ```java
  20. public class Test3 {
  21. public static Optional<Insurance> getCarInsuranceName(Optional<Person> person, Optional<Car> car) {
  22. if (person.isPresent() && car.isPresent()) {
  23. return Optional.of(findInsurance(person.get(), car.get()));
  24. } else {
  25. return Optional.empty();
  26. }
  27. }
  28. private static Insurance findInsurance(Person person, Car car) {
  29. Insurance insurance = new Insurance();
  30. // 对比数据
  31. // 返回某个数据
  32. return insurance;
  33. }
  34. }
  • 但是和之前的if-else非常之类似。以不解包的方式组合两个Optional对象。 ```java public class Test3 {

    public static Optional getCarInsuranceName(Optional person, Optional car) {

    1. return person.flatMap(p -> car.map(c -> findInsurance(p, c)));

    }

    private static Insurance findInsurance(Person person, Car car) {

    1. Insurance insurance = new Insurance();
    2. // 对比数据
    3. // 返回某个数据
    4. return insurance;

    } }

  1. <a name="EQqHT"></a>
  2. # 实例
  3. - 这是个不伦不类的举例
  4. - 其表达的意思是,调用服务,返回一个 `Optional<Order>`然后根据 `isPresent()`判断是否执行
  5. ```java
  6. public class Test1 {
  7. private static final OrderService ORDER_SERVICE = new OrderService();
  8. public static void main(String[] args) {
  9. printOrder("TX");
  10. }
  11. private static void printOrder(String oid) {
  12. Optional<Order> orderOptional = getOrder(oid);
  13. if (!orderOptional.isPresent()) {
  14. return;
  15. }
  16. System.out.println(orderOptional.get().getOid());
  17. }
  18. private static Optional<Order> getOrder(String oid) {
  19. Order order = ORDER_SERVICE.queryOrderByOid(oid);
  20. return Optional.ofNullable(order);
  21. }
  22. }
  23. @Data
  24. @AllArgsConstructor
  25. class Order {
  26. private String oid;
  27. }
  28. class OrderService {
  29. public Order queryOrderByOid(String oid) {
  30. if (!"TX0010".equals(oid)) {
  31. return null;
  32. }
  33. return new Order(oid);
  34. }
  35. }

Optional类的方法

方法 描述
empty 返回一个空的Optional实例
filter 如果值存在并且满足提供的谓词,就返回该值的Optional对象;否则返回一个空的Optional实例
flatMap 如果值存在,就对该值执行提供的mapping函数调用,返回一个Optional类型的值;否则返回一个空的Optional实例
get 如果值存在,返回值;否则抛出NoSuthElementException异常
ifPresent 如果值存在,就使用该值的方法方法调用,否则什么也不做
isPresent 存在返回true,否则返回false
map 如果值存在,就对该值执行提供的mapping函数调用
of 对目标值封装为Optional返回,如果为null,抛出NullPointerException
ofNullable 对目标值封装为Optional返回,如果为null,返回空的Optional对象
orElse 目标有值则返回,否则返回默认值
orElseGet 目标有值则返回,否则返回一个由指定的Supplier接口生成的值
orElseThrow 目标有值则返回,否则返回一个由指定的Supplier接口生成的异常

小结

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

    参考文章

  • 《Java 8 in Action》

  • 《Java8函数式编程》