目录

用Optional取代null

本章内容

  • null引用引发的问题,以及为什么要避免null引用
  • 从null到Optional:以null安全的方式重写你的域模型
  • 让Optional发光发热: 去除代码中对null的检查
  • 读取Optional中可能值的几种方法
  • 对可能缺失值的再思考

1965年,英国一位名为Tony Hoare的计算机科学家在设计ALGOL W语言时提出了null引用 的想法。ALGOL W是第一批在堆上分配记录的类型语言之一。Hoare选择null引用这种方式,“只 是因为这种方法实现起来非常容易”。虽然他的设计初衷就是要“通过编译器的自动检测机制, 确保所有使用引用的地方都是绝对安全的”,他还是决定为null引用开个绿灯,因为他认为这是为“不存在的值”建模最容易的方式。很多年后,他开始为自己曾经做过这样的决定而后悔不迭, 把它称为“我价值百万的重大失误”。我们已经看到它带来的后果——程序员对对象的字段进行 检查,判断它的值是否为期望的格式,最终却发现我们查看的并不是一个对象,而是一个空指针, 它会立即抛出一个让人厌烦的NullPointerException异常。

如何为缺失的值建模

假设你需要处理下面这样的嵌套对象,这是一个拥有汽车及汽车保险的客户。

代码清单10-1Person/Car/Insurance的数据模型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person {
    private Car car;

    public Car getCar() {
        return car;
    }
}

class Car {
    private Insurance insurance;

    public Insurance getInsurance() {
        return insurance;
    }
}

class Insurance {
    private String name;

    public String getName() {
        return name;
    }
}

那么,下面这段代码存在怎样的问题呢?

1
2
3
public String getCarInsuranceName(Person person) {
        return person.getCar().getInsurance().getName();
    }

这段代码看起来相当正常,但是现实生活中很多人没有车。所以调用getCar方法的结果会 怎样呢?在实践中,一种比较常见的做法是返回一个null引用,表示该值的缺失,即用户没有 车。而接下来,对getInsurance的调用会返回null引用的insurance,这会导致运行时出现 一个NullPointerException,终止程序的运行。但这还不是全部。如果返回的person值为null 会怎样?如果getInsurance的返回值也是null,结果又会怎样?

采用防御式检查减少NullPointerException

怎样做才能避免这种不期而至的NullPointerException呢?通常,你可以在需要的地方添 加null的检查(过于激进的防御式检查甚至会在不太需要的地方添加检测代码),并且添加的方式 往往各有不同。下面这个例子是我们试图在方法中避免NullPointerException的第一次尝试。

代码清单10-2 null-安全的第一种尝试:深层质疑

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public String getCarInsuranceName(Person person) {
        if (person != null) {
            Car car = person.getCar();
            if (car != null) {
                Insurance insurance = car.getInsurance();
                if (insurance != null) {
                    return insurance.getName();
                }
            }
        }
        return "Unknown";
    }

每个 null 检查都会增加调 用链上剩余代码的嵌套层数

这个方法每次引用一个变量都会做一次null检查,如果引用链上的任何一个遍历的解变量 值为null,它就返回一个值为“Unknown”的字符串。唯一的例外是保险公司的名字,你不需 要对它进行检查,原因很简单,因为任何一家公司必定有个名字。注意到了吗,由于你掌握业务领域的知识,避免了最后这个检查,但这并不会直接反映在你建模数据的Java类之中。

我们将代码清单10-2标记为“深层质疑”,原因是它不断重复着一种模式:每次你不确定一 个变量是否为null时,都需要添加一个进一步嵌套的if块,也增加了代码缩进的层数。很明显, 这种方式不具备扩展性,同时还牺牲了代码的可读性。面对这种窘境,你也许愿意尝试另一种方 案。下面的代码清单中,我们试图通过一种不同的方式避免这种问题。

代码清单10-3 null-安全的第二种尝试:过多的退出语句

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public String getCarInsuranceName(Person person) {
        if (person == 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语句块,采用了一种不同的策略:每次你遭遇null 变量,都返回一个字符串常量“Unknown”。然而,这种方案远非理想,现在这个方法有了四个 截然不同的退出点,使得代码的维护异常艰难。更糟的是,发生null时返回的默认值,即字符 串“Unknown”在三个不同的地方重复出现——出现拼写错误的概率不小!当然,你可能会说, 我们可以用把它们抽取到一个常量中的方式避免这种问题。

进一步而言,这种流程是极易出错的;如果你忘记检查了那个可能为null的属性会怎样? 通过这一章的学习,你会了解使用null来表示变量值的缺失是大错特错的。你需要更优雅的方式来对缺失的变量值建模。

null带来的种种问题

让我们一起回顾一下到目前为止进行的讨论,在Java程序开发中使用null会带来理论和实际 操作上的种种问题。

  • 它是错误之源。

    NullPointerException是目前Java程序开发中最典型的异常。

  • 它会使你的代码膨胀。

    它让你的代码充斥着深度嵌套的null检查,代码的可读性糟糕透顶。

  • 它自身是毫无意义的。

    null自身没有任何的语义,尤其是,它代表的是在静态类型语言中以一种错误的方式对缺失变量值的建模。

  • 它破坏了Java的哲学。

    Java一直试图避免让程序员意识到指针的存在,唯一的例外是:null指针。

  • 它在Java的类型系统上开了个口子。

    null并不属于任何类型,这意味着它可以被赋值给任意引用类型的变量。这会导致问题, 原因是当这个变量被传递到系统中的另一个部分后,你将无法获知这个null变量最初的赋值到底是什么类型。

为了解业界针对这个问题给出的解决方案,我们一起简单看看其他语言提供了哪些功能。

其他语言中null的替代品

近年来出现的语言,比如Groovy,通过引入安全导航操作符(Safe Navigation Operator,标 记为?)可以安全访问可能为null的变量。为了理解它是如何工作的,让我们看看下面这段Groovy 代码,它的功能是获取某个用户替他的车保险的保险公司的名称:

1
def carInsuranceName = person?.car?.insurance?.name

这段代码的表述相当清晰。 person 对象可能没有 car 对象, 你试图通过赋一个 null 给 Person对象的car引用,对这种可能性建模。类似地,car也可能没有insurance。Groovy的安 全导航操作符能够避免在访问这些可能为null引用的变量时抛出NullPointerException,在调用链中的变量遭遇null时将null引用沿着调用链传递下去,返回一个null。

关于Java 7的讨论中曾经建议过一个类似的功能,不过后来又被舍弃了。不知道为什么,我 们在Java中似乎并不特别期待出现一种安全导航操作符, 几乎所有的Java程序员碰到 NullPointerException时的第一冲动就是添加一个if语句,在调用方法使用该变量之前检查 它的值是否为null,快速地搞定问题。如果你按照这种方式解决问题,丝毫不考虑你的算法或者你的数据模型在这种状况下是否应该返回一个null,那么你其实并没有真正解决这个问题, 只是暂时地掩盖了问题,使得下次该问题的调查和修复更加困难,而你很可能就是下个星期或下 个月要面对这个问题的人。刚才的那种方式实际上是掩耳盗铃,只是在清扫地毯下的灰尘。而 Groovy的null安全解引用操作符也只是一个更强大的扫把,让我们可以毫无顾忌地犯错。你不会忘记做这样的检查,因为类型系统会强制你进行这样的操作。

另一些函数式语言,比如Haskell、Scala,试图从另一个角度处理这个问题。Haskell中包含 了一个Maybe类型,它本质上是对optional值的封装。Maybe类型的变量可以是指定类型的值,也可以什么都不是。但是它并没有null引用的概念。Scala有类似的数据结构,名字叫Option[T], 它既可以包含类型为T的变量,也可以不包含该变量,我们在第15章会详细讨论这种类型。要使 用这种类型,你必须显式地调用Option类型的available操作,检查该变量是否有值,而这其 实也是一种变相的“null检查”。

好了,我们似乎有些跑题了,刚才这些听起来都十分抽象。你可能会疑惑:“那么Java 8提供 了什么呢?”嗯, 实际上Java 8从“ optional 值”的想法中吸取了灵感, 引入了一个名为 java.util.Optional的新的类。这一章里,我们会展示使用这种方式对可能缺失的值建模, 而不是直接将null赋值给变量所带来的好处。我们还会阐释从null到Optional的迁移,你需要 反思的是:如何在你的域模型中使用optional值。最后,我们会介绍新的Optional类提供的功 能,并附几个实际的例子,展示如何有效地使用这些特性。最终,你会学会如何设计更好的API—— 用户只需要阅读方法签名就能知道它接受的optional值是否可能会为null。

Optional入门

汲取Haskell和Scala的灵感,Java 8中引入了一个新的类java.util.Optional<T>。这 是一个封装Optional值的类。举例来说,使用新的类意味着,如果你知道一个人可能有也可能没有车,那么Person类内部的car变量就不应该声明为Car,遭遇某人没有车时把null引用赋值给它,而是应该像图10-1那样直接将其声明为Optional<Car>类型。

https://gitee.com/lienhui68/picStore/raw/master/null/20200813155845.png

变量存在时,Optional类只是对类简单封装。变量不存在时,缺失的值会被建模成一个“空” 的Optional对象,由方法Optional.empty()返回。Optional.empty()方法是一个静态工厂 方法,它返回Optional类的特定单一实例。你可能还有疑惑,null引用和Optional.empty() 有什么本质的区别吗?从语义上,你可以把它们当作一回事儿,但是实际中它们之间的差别非常 大 : 如果你尝试解引用一个null , 一定会触 发NullPointerException , 不过使用Optional.empty()就完全没事儿,它是Optional类的一个有效对象,多种场景都能调用,非 常有用。关于这一点,接下来的部分会详细介绍。

使用Optional而不是null的一个非常重要而又实际的语义区别是,第一个例子中,我们 在声明变量时使用的是Optional<Car>类型,而不是Car类型,这句声明非常清楚地表明了这里发生变量缺失是允许的。与此相反,使用Car这样的类型,可能将变量赋值为null,这意味着你需要独立面对这些,你只能依赖你对业务模型的理解,判断一个null是否属于该变量的有效范畴。

牢记上面这些原则,你现在可以使用Optional类对代码清单10-1中最初的代码进行重构, 结果如下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person {
    private Optional<Car> car; // 人可能有车,也可能没 有车,因此将这个字段 声明为Optional

    public Optional<Car> getCar() {
        return car;
    }
}

class Car {
    private Optional<Insurance> insurance; // 车可能进行了保险,也可 能没有保险,所以将这个 字段声明为Optional

    public Optional<Insurance> getInsurance() {
        return insurance;
    }
}

class Insurance {
    private String name; // 保险公司必 须有名字

    public String getName() {
        return name;
    }
}

发现Optional是如何丰富你模型的语义了吧。代码中person引用的是Optional<Car>, 而 car引用的是Optional<Insurance>,这种方式非常清晰地表达了你的模型中一个person 可能拥有也可能没有car的情形,同样,car可能进行了保险,也可能没有保险。

与此同时, 我们看到 insurance 公司的名称被声明成 String 类型, 而不是 Optional<String>这非常清楚地表明声明为insurance公司的类型必须提供公司名称。使用这种方式, 一旦解引用insurance公司名称时发生NullPointerException,你就能非常确定地知道出错 的原因,不再需要为其添加null的检查,因为null的检查只会掩盖问题,并未真正地修复问题。 insurance公司必须有个名字,所以,如果你遇到一个公司没有名称,你需要调查你的数据出了 什么问题,而不应该再添加一段代码,将这个问题隐藏。

在你的代码中始终如一地使用Optional,能非常清晰地界定出变量值的缺失是结构上的问题,还是你算法上的缺陷,抑或是你数据中的问题。另外,我们还想特别强调,引入Optional 类的意图并非要消除每一个null引用。与此相反,它的目标是帮助你更好地设计出普适的API, 让程序员看到方法签名,就能了解它是否允许接受一个null的值。这种强制会让你更积极地将变量从Optional中解包出来,直面缺失的变量值。

应用 Optional 的几种模式

到目前为止,一切都很顺利;你已经知道了如何使用Optional类型来声明你的域模型,也 了解了这种方式与直接使用null引用表示变量值的缺失的优劣。但是,我们该如何使用呢?用 这种方式能做什么,或者怎样使用Optional封装的值呢?

创建 Optional 对象

使用Optional之前,你首先需要学习的是如何创建Optional对象。完成这一任务有多种 方法。

声明一个空的Optional

正如前文已经提到,你可以通过静态工厂方法Optional.empty,创建一个空的Optional 对象:

1
Optional<Car> optCar = Optional.empty();

依据一个非空值创建Optional

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

1
Optional<Car> optCar = Optional.of(car);

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

可接受null的Optional

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

1
Optional<Car> optCar = Optional.ofNullable(car);

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

你可能已经猜到,我们还需要继续研究“如何获取Optional变量中的值”。尤其是,Optional 提供了一个get方法,它能非常精准地完成这项工作,我们在后面会详细介绍这部分内容。不过 get方法在遭遇到空的Optional对象时也会抛出异常,所以不按照约定的方式使用它,又会让 我们再度陷入由null引起的代码维护的梦魇。因此,我们首先从无需显式检查的Optional值的 使用入手,这些方法与Stream中的某些操作极其相似。

使用 map 从 Optional 对象中提取和转换值

从对象中提取信息是一种比较常见的模式。比如,你可能想要从insurance公司对象中提取 公司的名称。提取名称之前,你需要检查insurance对象是否为null,代码如下所示:

1
2
3
4
String name = null; 
if(insurance != null){ 
  	name = insurance.getName(); 
}

为了支持这种模式,Optional提供了一个map方法。它的工作方式如下(这里,我们继续 借用了代码清单10-4的模式):

1
2
3
4
5
6
7
8
public static void main(String[] args) {
        Insurance insurance = null;
        Optional<Insurance> optionalInsurance = Optional.ofNullable(insurance);
        Optional<String> name = optionalInsurance.map(Insurance::getName);
        System.out.println(name.orElse("undefined"));
}
// 运行结果
undefined

从概念上,这与我们在第4章和第5章中看到的流的map方法相差无几。map操作会将提供的函数应用于流的每个元素。你可以把Optional对象看成一种特殊的集合数据,它至多包含一个 元素。如果Optional包含一个值,那函数就将该值作为参数传递给map,对该值进行转换。如 果Optional为空,就什么也不做。图10-2对这种相似性进行了说明,展示了把一个将正方形转 换为三角形的函数,分别传递给正方形和Optional正方形流的map方法之后的结果。

https://gitee.com/lienhui68/picStore/raw/master/null/20200813162314.png

这看起来挺有用,但是你怎样才能应用起来,重构之前的代码呢?前文的代码里用安全的方 式链接了多个方法。

1
2
3
public String getCarInsuranceName(Person person) { 
	return person.getCar().getInsurance().getName(); 
}

为了达到这个目的,我们需要求助Optional提供的另一个方法flatMap。

使用 flatMap 链接 Optional 对象

由于我们刚刚学习了如何使用map,你的第一反应可能是我们可以利用map重写之前的代码, 如下所示:

1
2
Optional<Person> optionalPerson = Optional.of(null);
        Optional<String> name = optionalPerson.map(Person::getCar).map(Car::getInsurance).map(Insurance::getName);

不幸的是,这段代码无法通过编译。为什么呢?optPerson是Optional<Person>类型的 变量, 调用map方法应该没有问题。但getCar返回的是一个Optional<Car>类型的对象(如代 码清单10-4所示),这意味着map操作的结果是一个Optional<Optional<Car>>类型的对象。因 此,它对getInsurance的调用是非法的,因为最外层的optional对象包含了另一个optional 对象的值,而它当然不会支持getInsurance方法。图10-3说明了你会遭遇的嵌套式optional 结构。

https://gitee.com/lienhui68/picStore/raw/master/null/20200813163339.png

所以, 我们该如何解决这个问题呢?让我们再回顾一下你刚刚在流上使用过的模式: flatMap方法。使用流时,flatMap方法接受一个函数作为参数,这个函数的返回值是另一个流。

1
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

这个方法会应用到流中的每一个元素,最终形成一个新的流的流(Stream<Stream>)。但是flagMap会用流的内容替换每个新生成的流。换句话说,由方法生成的各个流会被合并或者扁平化为一个单一的流。这里 你希望的结果其实也是类似的,但是你想要的是将两层的optional合并为一个。

跟图10-2类似,我们借助图10-4来说明flatMap方法在Stream和Optional类之间的相似性。

https://gitee.com/lienhui68/picStore/raw/master/null/20200813163628.png

这个例子中,传递给流的flatMap方法会将每个正方形转换为另一个流中的两个三角形。那 么,map操作的结果就包含有三个新的流,每一个流包含两个三角形,但flatMap方法会将这种 两层的流合并为一个包含六个三角形的单一流。类似地,传递给optional的flatMap方法的函 数会将原始包含正方形的optional对象转换为包含三角形的optional对象。如果将该方法传递 给map方法,结果会是一个Optional对象,而这个Optional对象中包含了三角形;但flatMap 方法会将这种两层的Optional对象转换为包含三角形的单一Optional对象。

  1. 使用Optional获取car的保险公司名称

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    
    public class Demo8 {
        public static void main(String[] args) {
            System.out.println(getCarInsuranceName(Optional.ofNullable(null)));
        }
       
        public static String getCarInsuranceName(Optional<Person> person) {
            return person.flatMap(Person::getCar)
                    .flatMap(Car::getInsurance)
                    .map(Insurance::getName)
                    .orElse("undefined");
        }
       
    }
       
    class Person {
        private Optional<Car> car; // 人可能有车,也可能没 有车,因此将这个字段 声明为Optional
       
        public Optional<Car> getCar() {
            return car;
        }
    }
       
    class Car {
        private Optional<Insurance> insurance; // 车可能进行了保险,也可 能没有保险,所以将这个 字段声明为Optional
       
        public Optional<Insurance> getInsurance() {
            return insurance;
        }
    }
       
    class Insurance {
        private String name; // 保险公司必 须有名字
       
        public String getName() {
            return name;
        }
    }
    

    我们可以看到,处理潜在可能缺失的值时, 使用Optional具有明显的优势。这一次,你可以用非常容易却又普适的方法实现之前你期望的 效果——不再需要使用那么多的条件分支,也不会增加代码的复杂性。

    我们再一次看到这种方式的优点,它通过类型系统让你的域模型中隐藏的知识显式地体现在你的代码中,换句话说,你永远都不应该忘记语言的首要功能就是沟通,即使对程序设计语言而 言也没有什么不同。声明方法接受一个Optional参数,或者将结果作为Optional类型返回,让 你的同事或者未来你方法的使用者,很清楚地知道它可以接受空值,或者它可能返回一个空值。

  2. 使用Optional解引用串接的Person/Car/Insurance对象

    Optional<Person>对象,我们可以结合使用之前介绍的map和flatMap方法,从Person 中解引用出Car,从Car中解引用出Insurance,从Insurance对象中解引用出包含insurance 公司名称的字符串。图10-5对这种流水线式的操作进行了说明。

    https://gitee.com/lienhui68/picStore/raw/master/null/20200813164341.png

    这里,我们从以Optional封装的Person入手,对其调用flatMap(Person::getCar)。如 前所述, 这种调用逻辑上可以划分为两步。

    第一步, 某个 Function 作为参数, 被传递给由Optional封装的Person对象,对其进行转换。这个场景中,Function的具体表现是一个方法 引用,即对Person对象的getCar方法进行调用。由于该方法返回一个Optional<Car>类型的 对象,Optional内的Person也被转换成了这种对象的实例,结果就是一个两层的Optional对 象,最终它们会被flagMap操作合并。如果你对一个空的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>,它可能包含保险公司的 名称,也可能为空。代码清单10-5中,我们使用了一个名为orElse的方法,当Optional的值为 空时,它会为其设定一个默认值。除此之外,还有很多其他的方法可以为Optional设定默认值, 或者解析出Optional代表的值。接下来我们会对此做进一步的探讨。

在域模型中使用Optional,以及为什么它们无法序列化

在代码清单10-4中,我们展示了如何在你的域模型中使用Optional,将允许缺失或者暂无定义的变量值用特殊的形式标记出来。然而,Optional类设计者的初衷并非如此,他们构思时怀揣的是另一个用例。这一点,Java语言的架构师Brian Goetz曾经非常明确地陈述过,Optional的设计初衷仅仅是要支持能返回Optional对象的语法。

由于 Optional 类设计时就没特别考虑将其作为类的字段使用, 所以它也并未实现Serializable接口。 由于这个原因,如果你的应用使用了某些要求序列化的库或者框架,在域模型中使用Optional,有可能引发应用程序故障。 然而,我们相信,通过前面的介绍,你已经看到用Optional声明域模型中的某些类型是个不错的主意,尤其是你需要遍历有可能全部或部分为空,或者可能不存在的对象时。如果你一定要实现序列化的域模型,作为替代方案,我们建议你像下面这个例子那样,提供一个能访问声明为Optional、变量值可能缺失的接口,代码清单如下:

1
2
3
4
5
6
public class Person {
	private Car car;
	public Optional<Car> getCarAsOptional() {
		return Optional.ofNullable(car);
	}
}

默认行为及解引用 Optional 对象

我们决定采用orElse方法读取这个变量的值,使用这种方式你还可以定义一个默认值,遭 遇空的Optional变量时,默认值会作为该方法的调用返回值。Optional类提供了多种方法读取 Optional实例中的变量值。

  • get()是这些方法中最简单但又最不安全的方法。如果变量存在,它直接返回封装的变量 值,否则就抛出一个NoSuchElementException异常。所以,除非你非常确定Optional 变量一定包含值,否则使用这个方法是个相当糟糕的主意。此外,这种方式即便相对于嵌套式的null检查,也并未体现出多大的改进。
  • orElse(T other)是我们在代码清单10-5中使用的方法,正如之前提到的,它允许你在 Optional对象不包含值时提供一个默认值。
  • orElseGet(Supplier<? extends T> other)是orElse方法的延迟调用版,Supplier 方法只有在Optional对象不含值时才执行调用。如果创建默认值是件耗时费力的工作, 你应该考虑采用这种方式(借此提升程序的性能),或者你需要非常确定某个方法仅在 Optional为空时才进行调用,也可以考虑该方式(这种情况有严格的限制条件)。
  • orElseThrow(Supplier<? extends X> exceptionSupplier)和get方法非常类似, 它们遭遇Optional对象为空时都会抛出一个异常,但是使用orElseThrow你可以定制希 望抛出的异常类型。
  • ifPresent(Consumer<? super T>)让你能在变量值存在时执行一个作为参数传入的 方法,否则就不进行任何操作。

两个 Optional 对象的组合

现在,我们假设你有这样一个方法,它接受一个Person和一个Car对象,并以此为条件对外 部提供的服务进行查询,通过一些复杂的业务逻辑,试图找到满足该组合的最便宜的保险公司:

1
2
3
4
5
 public Insurance findCheapestInsurance(Person person, Car car) {
        // 不同的保险公司提供的查询服务
        // 对比所有数据
        return cheapestCompany;
    }

我们还假设你想要该方法的一个null-安全的版本,它接受两个Optional对象作为参数, 返回值是一个Optional对象,如果传入的任何一个参数值为空,它的返回值亦为 空。Optional类还提供了一个isPresent方法,如果Optional对象包含值,该方法就返回true, 所以你的第一想法可能是通过下面这种方式实现该方法:

1
2
3
4
5
6
7
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对象

你可以像使用三元操作符那样,无需任何条件判断的结构,以一行语句实现该方法,代码如下。

1
2
3
4
5
6
7
8
9
public Insurance findCheapestInsurance(Person person, Car car) {
        // 不同的保险公司提供的查询服务
        // 对比所有数据
        return cheapestCompany;
    }

    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方法,完成期望的操作。

Optional类和Stream接口的相似之处远不止map和flatMap这两个方法。还有第三个方法 filter,它的行为在两种类型之间也极其相似,我们在接下来的一节会进行介绍。

使用 filter 剔除特定的值

你经常需要调用某个对象的方法,查看它的某些属性。比如,你可能需要检查保险公司的名 称是否为“Cambridge-Insurance”。为了以一种安全的方式进行这些操作,你首先需要确定引用指 向的Insurance对象是否为null,之后再调用它的getName方法,如下所示:

1
2
3
4
5
6
public static void main(String[] args) {
        Insurance insurance = null;
        if (insurance != null && "CambridgeInsurance".equals(insurance.getName())) {
            System.out.println("ok");
        }
    }

使用Optional对象的filter方法,这段代码可以重构如下:

1
2
3
4
5
6
public static void main(String[] args) {
    Optional<Insurance> optionalInsurance = Optional.empty();
    optionalInsurance.filter(insurance -> "CambridgeInsurance".equals(insurance.getName())).ifPresent(
            insurance -> System.out.println("hello")
    );
}

filter方法接受一个谓词作为参数。如果Optional对象的值存在,并且它符合谓词的条件, filter 方法就返回其值;否则它就返回一个空的 Optional 对象。 如果你还记得我们可以将 Optional看成最多包含一个元素的Stream对象,这个方法的行为就非常清晰了。如果Optional 对象为空,它不做任何操作,反之,它就对Optional对象中包含的值施加谓词操作。如果该操作的结果为 true , 它不做任何改变, 直接返回该 Optional 对象, 否则就将该值过滤掉, 将 Optional的值置空。

Optional类中方法的分类和概括

https://gitee.com/lienhui68/picStore/raw/master/null/20200813171954.png

使用 Optional 的实战示例

有效地使用Optional类意味着你需要对如何处理潜在缺失值进行全面的反思。这种反思不仅仅限于你曾经写过的代码,更重要的可能是,你如何与原生Java API实现共 存共赢。

实际上,我们相信如果Optional类能够在这些API创建之初就存在的话,很多API的设计编写可能会大有不同。为了保持后向兼容性,我们很难对老的Java API进行改动,让它们也使用 Optional,但这并不表示我们什么也做不了。你可以在自己的代码中添加一些工具方法,修复 或者绕过这些问题,让你的代码能享受Optional带来的威力。我们会通过几个实际的例子讲解 如何达到这样的目的。

用 Optional 封装可能为 null 的值

现存Java API几乎都是通过返回一个null的方式来表示需要值的缺失,或者由于某些原因计 算无法得到该值。比如,如果Map中不含指定的键对应的值,它的get方法会返回一个null。但 是,正如我们之前介绍的,大多数情况下,你可能希望这些方法能返回一个Optional对象。你 无法修改这些方法的签名,但是你很容易用Optional对这些方法的返回值进行封装。我们接着 用 Map做例子,假设你有一个Map<String, Object>方法,访问由key索引的值时,如果map 中没有与key关联的值,该次调用就会返回一个null。

1
Object value = map.get("key");

使用Optional封装map的返回值,你可以对这段代码进行优化。要达到这个目的有两种方 式:你可以使用笨拙的if-then-else判断语句,毫无疑问这种方式会增加代码的复杂度;或者你可以采用我们前文介绍的Optional.ofNullable方法:

1
Optional<Object> value = Optional.ofNullable(map.get("key"));

每次你希望安全地对潜在为null的对象进行转换,将其替换为Optional对象时,都可以考 虑使用这种方法。

异常与 Optional 的对比

由于某种原因,函数无法返回某个值,这时除了返回null,Java API比较常见的替代做法是 抛出一个异常。 这种情况比较典型的例子是使用静态方法 Integer.parseInt(String) , 将 String 转换为 int 。 在这个例子中, 如果 String 无法解析到对应的整型, 该方法就抛出一个 NumberFormatException。最后的效果是,发生String无法转换为int时,代码发出一个遭遇 非法参数的信号,唯一的不同是,这次你需要使用try/catch 语句,而不是使用if条件判断来 控制一个变量的值是否非空。

你也可以用空的Optional对象,对遭遇无法转换的String时返回的非法值进行建模,这时 你期望parseInt的返回值是一个optional。我们无法修改最初的Java方法,但是这无碍我们进 行需要的改进,你可以实现一个工具方法,将这部分逻辑封装于其中,最终返回一个我们希望的 Optional对象,代码如下所示。

https://gitee.com/lienhui68/picStore/raw/master/null/20200813172450.png

我们的建议是,你可以将多个类似的方法封装到一个工具类中,让我们称之为 Optiona- lUtility 。 通过这种方式, 你以后就能直接调用 OptionalUtility.stringToInt 方法, 将 String 转换为一个 Optional<Integer> 对象, 而不再需要记得你在其中封装了笨拙的 try/catch的逻辑了。

基础类型的Optional对象,以及为什么应该避免使用它们

不 知 道 你 注 意 到 了 没 有 , 与 Stream 对 象 一 样 , Optional 也 提 供 了 类 似 的 基 础 类 型——OptionalInt、OptionalLong以及OptionalDouble——所以代码清单10-6中的方法可 以不返回Optional<Integer>,而是直接返回一个OptionalInt类型的对象。第5章中,我们讨论过使用基础类型Stream的场景,尤其是如果Stream对象包含了大量元素,出于性能的考量, 使用基础类型是不错的选择,但对Optional对象而言,这个理由就不成立了,因为Optional 对象最多只包含一个值。

我们不推荐大家使用基础类型的 Optional , 因为基础类型的 Optional 不支持 map 、flatMap以及filter方法,而这些却是Optional类最有用的方法(正如我们在10.2节所看到的 那样)。此外,与Stream一样,Optional对象无法由基础类型的Optional组合构成,所以,举例而言,如果代码清单10-6中返回的是OptionalInt类型的对象,你就不能将其作为方法引用传递给另一个Optional对象的flatMap方法。

把所有内容整合起来

为了展示之前介绍过的Optional类的各种方法整合在一起的威力,我们假设你需要向你的 程序传递一些属性。为了举例以及测试你开发的代码,你创建了一些示例属性,如下所示:

1
2
3
4
5
6
7
Properties props = new Properties() {
        {
            props.setProperty("a", "5");
            props.setProperty("b", "true");
            props.setProperty("c", "-3");
        }
    };

现在,我们假设你的程序需要从这些属性中读取一个值,该值是以秒为单位计量的一段时间。 由于一段时间必须是正数,你想要该方法符合下面的签名:

1
public int readDuration(Properties props, String name)

即,如果给定属性对应的值是一个代表正整数的字符串,就返回该整数值,任何其他的情况都返 回0。为了明确这些需求,你可以采用JUnit的断言,将它们形式化:

1
2
3
4
assertEquals(5, readDuration(param, "a")); 
assertEquals(0, readDuration(param, "b")); 
assertEquals(0, readDuration(param, "c")); 
assertEquals(0, readDuration(param, "d"));

这些断言反映了初始的需求:如果属性是a,readDuration方法返回5,因为该属性对应的 字符串能映射到一个正数;对于属性b,方法的返回值是0,因为它对应的值不是一个数字;对于 c,方法的返回值是0,因为虽然它对应的值是个数字,不过它是个负数;对于d,方法的返回值 是0,因为并不存在该名称对应的属性。让我们以命令式编程的方式实现满足这些需求的方法, 代码清单如下所示。

https://gitee.com/lienhui68/picStore/raw/master/null/20200813173144.png

你可能已经预见,最终的实现既复杂又不具备可读性,呈现为多个由if语句及try/catch 块儿构成的嵌套条件。

使用Optional从属性中读取duration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Demo8 {

    Properties props = new Properties() {
        {
            setProperty("a", "5");
            setProperty("b", "true");
            setProperty("c", "-3");
        }
    };

    public int readDuration(Properties props, String name) {
        return Optional.ofNullable(props.getProperty(name)).flatMap(OptionalUtil::string2Int)
                .filter(i -> i > 0).orElse(0);
    }


    public static void main(String[] args) {
        Demo8 demo8 = new Demo8();
        assert demo8.readDuration(demo8.props, "a") == 5;
        assert demo8.readDuration(demo8.props, "b") == 0;
        assert demo8.readDuration(demo8.props, "c") == 0;
        assert demo8.readDuration(demo8.props, "d") == 0;
    }

}

class OptionalUtil {
    public static Optional<Integer> string2Int(String s) {
        try {
            return Optional.of(Integer.parseInt(s));
        } catch (NumberFormatException e) {
            return Optional.empty();
        }
    }
}

assert

默认情况下,JVM是关闭断言的。因此如果想使用断言调试程序,需要手动打开断言功能。在命令行模式下运行Java程序时可增加参数-enableassertions或者-ea打开断言。可通过-disableassertions或者-da关闭断言(默认情况,可有可无)。

断言的使用:

断言是通过关键字assert来定义的,一般的,它有两种形式。

  1. assert <bool expression> 比如 boolean isStudent = false; assert isStudent;

  2. assert <bool expression> : <message> 比如 boolean isSafe = false; assert isSafe : “Not Safe at all”;

注意到使用Optional和Stream时的那些通用模式了吗?它们都是对数据库查询过程的反 思,查询时,多种操作会被串接在一起执行。

小结

这一章中,你学到了以下的内容。

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