使用 Optional 解决烦人的空指针异常

使用 Optional 解决烦人的空指针异常

NullPointerException

空指针异常的发明人 Tony Hoare 曾在 2009 年为 NullPointerException 道歉,称之为『数十亿美元的错误』,原文如下:

I call it my billion-dollarmistake. It was the invention of the null reference in 1965. At that time, Iwas designing the first comprehensive type system for references in an objectoriented language (ALGOL W). My goal was to ensure that all use of referencesshould be absolutely safe, with checking performed automatically by thecompiler. But I couldn't resist the temptation to put in a null reference,simply because it was so easy to implement. This has led to innumerable errors,vulnerabilities, and system crashes, which have probably caused a billiondollars of pain and damage in the last forty years.

这可能是 Java 中最常见也是最烦人的异常了,Java 诞生之初的本意之一是要“屏蔽”掉令人头疼的指针,偏偏“空指针”这个异常又把“指针”这个概念带了回来。在我司内部的编码规范上很早就建议使用 Optional 这一 Java8 新增的机制来避免空指针异常,但是在我之前个人学习和写项目的时候,很少会去注意这个问题,基本都是采用 if 判空的方式来解决空指针异常,但是这种方式显然不是一种优雅的写代码方式,在后续编码的过程中应该摒弃这个不好的习惯。

空指针的发生与常见解决办法

以一个常见的例子,人 -> 车 -> 保险这一嵌套关系:

//人
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();
}

这个代码调用有一个非常明显的问题,就是客户可能是没有车的,或者有车是没有保险的,那么在 person.getCar() 或者 getInsurance() 期间都是可能为 null 的,这就会产生 NullPointerException 这一异常并中止程序的运行。

所以在初学阶段,或者直观上我们会这么做,用大量的 if 来进行判空,当前面一个对象不为 null 时才会调用后面的方法。

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

这个方法可行吗?当然是可行的,但是想必大家或多或少都明白或者听说过,代码中最好不要出现大量的 if/else 嵌套判断,一方面是可读性较差,另外一方面扩展性也不好,所以为了避免这种情况,“有经验”的老程序员们就会这样写业务代码:

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 代码,让代码整体更“简洁明了”,而且可以快速扩展。是的,我之前学习的时候就一直是这么做的,所以我现在认识到了问题所在:

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

Optional 对象

如何避免空指针异常,很多其它语言提供了简易的语法实现,如 Groovy 和 Kotlin,以下是 Grooy 示例:

displayName = user.name ? user.name: 'Anonymous'  
 
displayName = user.name ? : 'Anonymous'              

这种方式就很简便,而且直观。但是 Java 并没有这一语法,直到 Java8 才提出使用 java.util.Optinal<T> 对象来解决这个问题。这是一个封装 Optional 值的类。举例来说,使用新的类意味着,如果你知道一个人可能有也可能没有车,那么 Person 类内部的 car 变量就不应该声明为 Car ,遭遇某人没有车时把 null 引用赋值给它,而是应该直接将其声明为 Optional 类型。

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 ,能非常清晰地界定出变量值的缺失是结构上的问题,还是你算法上的缺陷,抑或是你数据中的问题。另外,我们还想特别强调,引入 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);
  • 可接受 null 的 Optional。使用静态工厂方法 Optional.ofNullable ,你可以创建一个允许 null 值的 Optional 对象,如果 car 是 null ,那么得到的 Optional 对象就是个空对象。
Optional<Car> optCar = Optional.ofNullable(car);

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

使用 flatMap 链接 Optional 对象

根据上面所学,如何重构之前的代码呢?

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

不幸的是,这段代码无法通过编译。为什么呢? optPersonOptional<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 对象

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

  1. get() 是这些方法中最简单但又最不安全的方法。如果变量存在,它直接返回封装的变量值,否则就抛出一个 NoSuchElementException 异常。所以,除非你非常确定 Optional变量一定包含值,否则使用这个方法是个相当糟糕的主意。此外,这种方式即便相对于嵌套式的 null 检查,也并未体现出多大的改进。

  2. orElse(T other) 是我们在代码清单10-5中使用的方法,正如之前提到的,它允许你在Optional 对象不包含值时提供一个默认值。

  3. orElseGet(Supplier<? extends T> other) 是 orElse 方法的延迟调用版, Supplier 方法只有在 Optional 对象不含值时才执行调用。如果创建默认值是件耗时费力的工作,你应该考虑采用这种方式(借此提升程序的性能),或者你需要非常确定某个方法仅在Optional 为空时才进行调用,也可以考虑该方式(这种情况有严格的限制条件)。

  4. orElseThrow(Supplier<? extends X> exceptionSupplier) 和 get 方法非常类似,它们遭遇 Optional 对象为空时都会抛出一个异常,但是使用 orElseThrow 你可以定制希望抛出的异常类型。

  5. 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 的值置空。

不是所有场景都需要使用 Optional

Optional 类应该作为可能有返回值函数的返回值类型。有人甚至建议 Optional 类应该改名为 OptionalReturn。

Optional 类不是为了避免所有的空指针类型机制。方法或构造函数输入参数强制性检查就仍然是有必要的。

在以下场景一般不建议使用 Optional 类。

  1. 领域模型层(非序列化)
  2. 数据传输对象(同上原因)
  3. 方法的输入参数
  4. 构造函数参数

本文由 Sanarous 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可,转载前请务必署名
本文链接:https://bestzuo.cn/posts/java-optional-nullpointer.html
最后更新于:2020-10-23 20:26:57

切换主题 | SCHEME TOOL  
>