用 Optional 取代 null

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

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

使用if防止 NullPointerException

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 ,它就返回一个值为“Unknown”的字符串。唯一的例外是保险公司的名字,你不需要对它进行检查,原因很简单,因为任何一家公司必定有个名字。注意到了吗,由于你掌握业务领域的知识,避免了最后这个检查,但这并不会直接反映在你建模数据的Java类之中。

使用if的第二次尝试

在上述代码中每次你不确定一个变量是否为 null 时,都需要添加一个进一步嵌套的 if 块,也增加了代码缩进的层数。很明显,这种方式不具备扩展性,同时还牺牲了代码的可读性。面对这种窘境,你也许愿意尝试另一种方案。如:

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建模带来的问题

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

使用Optional建模

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

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

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

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

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

创建 Optional 对象

  • 声明一个空的 Optional

通过静态工厂方法 Optional.empty ,创建一个空的 Optional对象:

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

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

  • 可接受 null 的 Optional

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

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

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

使用 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);

Optional的map方法同流的 map 方法相差无几。 map 操作会将提供的函数应用于流的每个元素。你可以把 Optional 对象看成一种特殊的集合数据,它至多包含一个元素。如果 Optional 包含一个值,那函数就将该值作为参数传递给 map ,对该值进行转换。如果 Optional 为空,就什么也不做。

使用 flatMap 链接 Optional 对象

上文中学习了如何使用 map,但是如何重构之前的如下代码呢?

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

你的第一反应可能是我们可以利用 map 重写之前的代码,如下所示:

Optional<Person> optPerson = Optional.of(person);
Optional<String> name = optPerson.map(Person::getCar)
    .map(Car::getInsurance)
    .map(Insurance::getName);

不幸的是,这段代码无法通过编译。为什么呢? optPerson 是 Optional<Person>
类型的变量, 调用 map 方法应该没有问题。但 getCar 返回的是一个 Optional<Car> 类型的对象,这意味着 map 操作的结果是一个 Optional<Optional<Car>> 类型的对象。因此,它对 getInsurance 的调用是非法的,因为最外层的 optional 对象包含了另一个 optional对象的值,而它当然不会支持 getInsurance 方法。

所以,我们该如何解决这个问题呢?让我们再回顾一下在流上使用过的模式:flatMap 方法。使用流时, flatMap 方法接受一个函数作为参数,这个函数的返回值是另一个流。这个方法会应用到流中的每一个元素,最终形成一个新的流的流。但是 flagMap 会用流的内容替换每个新生成的流。换句话说,由方法生成的各个流会被合并或者扁平化为一个单一的流。这里你希望的结果其实也是类似的,但是你想要的是将两层的 optional 合并为一个。如:

public String getCarInsuranceName(Optional<Person> person) {
return person.flatMap(Person::getCar)
    .flatMap(Car::getInsurance)
    .map(Insurance::getName)
    .orElse("Unknown");//如果 Optional 的结果值为空,设置默认值
}

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

Optional无法被序列化

我们展示了如何在你的域模型中使用 Optional ,将允许缺失或者暂
无定义的变量值用特殊的形式标记出来。然而, Optional 类设计者的初衷并非如此,他们构
思时怀揣的是另一个用例。这一点,Java语言的架构师Brian Goetz曾经非常明确地陈述过,
Optional 的设计初衷仅仅是要支持能返回 Optional 对象的语法。
由于 Optional 类设计时就没特别考虑将其作为类的字段使用,所以它也并未实现
Serializable 接口。由于这个原因,如果你的应用使用了某些要求序列化的库或者框架,在
域模型中使用 Optional ,有可能引发应用程序故障。然而,我们相信,通过前面的介绍,你
已经看到用 Optional 声明域模型中的某些类型是个不错的主意,尤其是你需要遍历有可能全
部或部分为空,或者可能不存在的对象时。如果你一定要实现序列化的域模型,作为替代方案,
我们建议你像下面这个例子那样,提供一个能访问声明为 Optional 、变量值可能缺失的接口,
代码清单如下:

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>) 让你能在变量值存在时执行一个作为参数传入的方法,否则就不进行任何操作。

使用 filter 剔除特定的值

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

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

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

Optional<Insurance> optInsurance = ...;
optInsurance.filter(insurance -> "CambridgeInsurance".equals(insurance.getName()))
    .ifPresent(x -> System.out.println("ok"));

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

另一个filter的使用示例

假设在我们的 Person / Car / Insurance 模型中, Person 还提供了一个方法可以取得
Person 对象的年龄,请使用下面的签名改写代码清单10-5中的 getCarInsuranceName 方法:

public String getCarInsuranceName(Optional<Person> person, int minAge)

找出年龄大于或者等于 minAge 参数的 Person 所对应的保险公司列表。

答案:你可以对 Optional 封装的 Person 对象进行 filter 操作,设置相应的条件谓词,即如果 person 的年龄大于 minAge 参数的设定值,就返回该值,并将谓词传递给 filter 方法,代码如下所示。

public String getCarInsuranceName(Optional<Person> person, int minAge) {
    return person.filter(p -> p.getAge() >= minAge)
        .flatMap(Person::getCar)
        .flatMap(Car::getInsurance)
        .map(Insurance::getName)
        .orElse("Unknown");
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值