Optional

Optional

一. 引文

对于Java编程来说,NullPointerException——空指针异常可以说是最经常遇到的异常了,这是引入null引用的代价,最初只是因为对“不存在的值”进行建模时这样实现起来非常容易,但后果就是使程序员需要对对象的字段进行检查,判断它的值是否为所期望,但结果却指向了一个空指针,并立即抛出了NullPointerException。如Java这些语言采用空引用的设计最初只是为了和老的语言保持兼容,但为此却付出了很多的代价。

函数式语言会通过更多的描述性数据类型来避免null,Java 8提供了 Optional<T> 类,使用它可以有效的避免NullPointerException。

使用Optional定义的Car类

Optional类是一个容器类,可以包含也可以不包含值,代表一个值存在或不存在,其方法可以明确地处理值不存在的情况。

  • isPresent() 在值存在时返回true,否则返回false。
  • isPresent(Consumer block) 在值存在时执行给定代码块。
  • T get() 在值存在时返回值,否则抛出异常NoSuchElement。
  • T orElse() 在值存在时返回值,否则返回一个默认值。

二. 如何为缺失的值建模

2.1 Java中的null

如下述代码便会抛出NullPointerException,导致程序的终止。

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
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 class Test {
public static void main(String[] args) {
getCarInsuranceName(new Person());//Exception in thread "main" java.lang.NullPointerException
}

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

为了避免空指针异常,通常我们要在需要的地方添加null的检查,如下述情况,是一种深层质疑的防御式检查。但很明显这种方式不具有扩展性,也牺牲了代码的可读性。

1
2
3
4
5
6
7
8
9
10
11
12
public static String getCarInsuranceName(Person person){//防御式检查减少NullPointerException
if(person != null){
Car car = person.getCar();
if(car != null){
Insurance insurance = car.getInsurance();
if(insurance != null){
return insurance.getName();//业务上设定公司必然有名字,所以可以避免了这一层检查,但不会直接反映在建模中
}
}
}
return "Unknown";
}

我们可以换一种尝试,避免深层递归的if语句块,而是每次遭遇null就返回字符串常量”Unknown”。这种方式使代码极难维护,且容易出现错误。

null导致的问题?

  • 错误之源:导致的NullPointerException是Java开发中最典型的异常。
  • 代码膨胀:使代码充斥着深度嵌套的null检查,代码的可读性糟糕透顶。
  • 毫无意义:null本身没有任何语义,它代表的是在静态类型语言中以一种错误的方式对缺失变量值得建模。
  • 破坏哲学:Java一直试图让程序员忽略指针的存在,但null指针是例外。
  • 留下漏洞:null不属于任何类型,可以被赋值给任意引用类型的变量,导致当这个变量被传递给系统的另一个部分后,无从获知这个null变量最初的赋值是何类型。

2.2 其他语言中null的替代品

稍微新一些的语言如Groovy,通过引入安全导航操作符(Safe Navigation Operator,?)可以安全的访问可能为null的变量。如下述代码,person对象可能没有car对象,但安全导航操作符可以避免抛出空指针异常,而是在遭遇null时将null引用沿着调用链传递下去,返回一个null。

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

我们遇到空指针异常时会自然的用if判断来规避异常,但这只是暂时的掩盖了问题,且使得以后的调查和修复变得更加困难。而安全导航操作符也只是一个更强大的扫把,让我们毫无顾虑的犯错。

另外一些语言如Haskell、Scala试图从另外一个角度来处理这个问题。Haskell中包含了一个Maybe类型,本质上是对optional值得封装。Maybe类型的变量可以是指定类型的值,也可以什么都不是,但没有null引用的概念。Scala类似的结构是Option[T],必须显式的调用Option类型的available操作,检查该变量是否有值,但其实也是一种变相的“null检查”。

Java 8则从这些中吸取了灵感,引入了 Optional<T> 的新类。

三. Optional类

当变量存在时,Optional只是对类简单封装。变量不存在时,缺失的值会被建模成一个空的Optional对象,由 Optional.empty() 返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final class Optional<T> {
/**
* Common instance for {@code empty()}.
*/
private static final Optional<?> EMPTY = new Optional<>();

private Optional() {
this.value = null;
}

public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
}

null引用和Optional.empty()有什么区别呢?如果尝试引用一个null则会导致空指针异常,但后者则可以,它是Optional类的一个有效对象,多种场景都可以调用。使用Optional就可以不用再通过理解业务模型来决定null是否属于一个变量的有效范畴。

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

public Optional<Car> getCar() {
return car;
}
}
public class Car {
private Optional<Insurance> insurance;

public Optional<Insurance> getInsurance() {
return insurance;
}
}
public class Insurance {
private String name;//保险公司必须有名字

public String getName() {
return name;
}
}

所以我们只要声明为Optional,表示此变量是允许缺失的,而未声明的也无需再用if判断来规避空指针异常,这样只会掩盖错误。始终如一的采用Optional可以非常清晰的界定出变量值的缺失是结构上的问题,还是算法上的缺陷,又或是数据中的问题。引入Optional并非是要消除null引用,而是帮助我们更好的设计出普适的API。

四. Optional应用

4.1 创建Optional对象

声明

1
2
3
4
5
6
7
8
//声明一个空的Optional对象
Optional<Car> optCar1 = Optional.empty();

//通过一个非空的值来创建Optional对象
Optional<Car> optCar2 = Optional.of(new Car());

//创建一个允许null值的Optional对象
Optional<Car> optCar3 = Optional.ofNullable(null);

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

Optional的map方法类似于流的map方法,将所提供的函数操作应用于流的每个元素,这里可以把Optional看作特殊的集合,至多只有一个元素,如果包含值就把值作为参数传递给map,然后对值进行转换;如果值为空,就什么也不做。

1
2
3
4
5
6
7
8
9
//传统方法
String name = null;
if(insurance != null){
name = insurance.getName();
}

//Optional
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);

Stream和Optional的map方法对比

4.3 使用flatMap链接Optional对象

如果我们想用Optional重构getCarInsuranceName方法,第一反应自然是用map方法重写,但改写后却无法通过编译。原因就是map方法返回的是 Optional<T> 类型的值,而getCar返回值是 Optional<Car> 对应T,所以第一次map返回 Optional<Optional<Car>> ,对其调用getInsurance自然是非法的。

1
2
3
4
5
6
7
8
9
10
11
12
//传统方法——未深度质疑
public static String getCarInsuranceName(Person person){
return person.getCar().getInsurance().getName();
}

//map重写
public static String getCarInsuranceName(Person person){
Optional<Person> optPerson = Optional.of(person);
Optional<String> name = optPerson.map(Person::getCar)
.map(Car::getInsurance)
.map(Insurance::getName);
}

这个时候我们应该回忆起流的flatMap方法,接受一个函数参数,函数的返回值是另一个流,将函数作用在流的每个元素上,最终获得一个新的流的流。由方法生成的流会扁平化为一个流,我们现在想要的就是把两层Optional合并为一个。

Stream和Optional的flatMap方法对比

1
2
3
4
5
6
7
8
//flatMap重写
public static String getCarInsuranceName(Person person){
Optional<Person> optPerson = Optional.of(person);
return optPerson.flatMap(Person::getCar)
.flatMap(Car::getInsurance)
.map(Insurance::getName)
.orElse("Unknown");
}

optPerson是Optional封装的Person,调用 flatMap(Person::getCar) 后,第一步,某个Function作为参数被传递给optPerson,对其进行转换。所以Function是调用getCar方法,返回一个 Optional<Car> 类型的对象, Optional<Person> 也会转换为 Optional<Optional<Car>> ,最终被flatMap操作合并。orElse方法在Optional值为空时返回一个默认值。

使用Optional解引用串接的Person/Car/Insurance

Optional无法序列化,因为设计者当时未考虑将其作为类的字段使用,仅仅是为了支持能返回Optional对象的语法,所以如果需要序列化时,类字段请不要包装Optional,提供一个能访问声明为Optional、变量值可能缺失的接口即可。

4.4 默认行为及解引用Optional对象

Optional类提供了多种方法读取Optional实例中的变量值。

  • get():变量存在,直接返回封装的变量值,否则抛出NoSuchElementException异常,相比null引用并未有多大改进。
  • orElse(T other):允许在Optional对象不包含值时提供一个默认值。
  • orElseThrow(Supplier<? extends X> exceptionSupplier):是orElse方法的延迟调用版,Supplier方法在Optional对象不包含值时执行调用。当创建默认值是一件耗时费力的工作时应该采用此方法(借此提高性能),或者需要某方法只有在Optional为空时才能调用。
  • ifPresent(Consumer<? super T>):在变量值存在时执行一个作为参数传入的方法,否则不做任何操作

4.5 两个Optional对象的组合

findCheapestInsurance方法接受两个参数Person和Car,通过一系列复杂的业务逻辑,找出满足该组合的最便宜的保险公司。如果我们想要实现一个null安全的版本,可能会如nullSafeFindCheapestInsurance这样实现。从参数我们就可以得知person和car都可能为空,这种时候方法的返回值不会包含任何值。但这种实现和findCheapestInsurance并没有太多本质上的区别。

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

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调用flatMap,如果是空值则传递的Lambda表达式不会执行,所以此次调用会直接返回一个空的Optional对象。如果不是空值,则此次调用会传递Lambda表达式,执行转换,并最终返回一个 Optional<Insurance> 对象。函数体中对第二个Optional对象car执行了map操作,若car为空值则返回一个空的Optional对象,所以整个nullSafeFindCheapestInsurance的最终返回值也是一个空的Optional对象。如果两个参数变量值都存在,那么作为参数传递给map方法的Lambda表达式能够使用这两个值安全的调用findCheapestInsurance。

1
2
3
public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Car> car){
return person.flatMap(p -> car.map(c -> findCheapestInsurance(p,c)));
}

4.6 使用filter剔除特定的值

在对引用对象的一些属性进行操作时,每次都要事先对null引用进行判断。如下代码中判断保险公司名称,这种情况可以通过filter方法来改写。

1
2
3
4
5
6
7
//检查保险公司名称是否为XX,首先要判断引用是否为null
if(insurance != null && "CambridgeInsurance".equals(insurance.getName())){
System.out.println("OK");
}
//filter方法改写
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
optInsurance.filter(insurance1 -> "CambridgeInsurance".equals(insurance.getName())).ifPresent(x -> System.out.println("OK"));

filter方法接受一个谓词作为参数,如果Optional对象值存在,且符合谓词的条件,filter方法会返回其值。否则返回一个空的Optional对象。

4.7 总结

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

第五节 Optional实战

有效的使用Optional意味着我们需要直面潜在缺失值的处理,为了保持后向兼容性,很难对老版本的Java API进行改动,使其也使用Optional,但可以通过一些工具方法来修复或绕过这些问题。

5.1 用Optional封装可能为null的值

Java API几乎都会以返回一个null来表示需要值的缺失,所以可以通过Optional封装返回值的方式进行优化。

1
2
3
4
5
Map<String,Object> map = new HashMap<>();
//当映射不包含键对应的值会返回null
Object value = map.get("key");
//Optional改写
Optional<Object> optValue = Optional.ofNullable(map.get("key"));

5.2 异常与Optional的对比

Java API中,因为一些原因函数无法返回某个值,这种情况除了返回null,比较常见的替代做法是抛出一个异常。比如Integer.parseInt(String),如果String无法解析到对应的整形,就会抛出一个NumberFormatException异常。所以我们就需要用try/catch语句来处理异常情况。

1
2
3
4
5
6
7
public static Optional<Integer> stringToInt(String s){
try {
return Optional.of(Integer.parseInt(s));
}catch (NumberFormatException ex){
return Optional.empty();
}
}

我们可以通过上述代码进行改写,通过空的Optional对象来对无法返回的值进行建模。虽然无法修改最初的Java方法,但无碍我们进行这些改进。建议把类似需要改进的方法封装到一个工具类中,称作OptionalUtily。直接调用OptionalUtily.stringToInt方法就将String转换为 Optional<Integer> 对象。

5.3 基础类型的Optional

Optional和流一样也提供了基础类版本——OptionalInt、OptionalDouble、OptionalLong,对于流来说基础类版本在适合的场景可以有效的提高性能,但对于Optional因为对象最多只包含一个值,所以并不会有性能上的差异。基础类型Optional不支持map、flatMap和filter方法这些最有用的方法。基础类型的Optional也不能组合构成新的Optional。


参考:

🔗 《Java8实战》