使用和避免使用空指针

guava探究系列(一)

Updated on 2019-07-02 19:28 (Created on: 2019-07-02 19:25)

前言

奆佬们关于空指针的看法:

Null sucks - Doug Lea(JCP,Java并发编程实战作者, Java巨佬)

I call it my billion-dollar mistake. - Sir C. A. R. Hoare, 空指针的发明者

按照Guava wiki的说法, 大部分的Google代码都是不支持使用空指针(下文用null表示空指针)的, 如接近95%的集合类都不支持使用null作为集合元素. 像Google这样的大公司明确不建议使用null自然是有其原由的, 不会无的放矢. 那具体原因是什么呢?待下文为你细细道来;

空指针的问题

空指针语意隐晦不明

null的语意并不了然明确, 即当一个函数返回null, 我们并不知道null的意思是指返回结果理应为空? 还是指函数没有达到预期结果, 返回null表示失败? 举个常见的例子, 当调用Map.get(key)获取key对应的value的时候, 返回结果为null; null是指找不到这个key对应的value? 还是说这个key对应的value本身就是null, 原来是通过Map.put(key,value)赋值的呢? null甚至可以是代表其他东西! 老实说, 当我们获得一个null, 我们并不清楚它究竟指的是啥, 除非有对应的javadoc 进行了说明.

空指针"暗藏杀机"

null除了语意不明外, 还非常容易在不经意间挖坑坑人. 例如有下面的代码:

private String testNull(String input) {
    if (random.nextInt() % 2 == 0) {
        return input;
    } else {
        return null;
    }
}

@Test
public void useNull() {
    String foo = testNull("foo#bar").split("#")[0];
    String bar = testNull("foo#bar").split("#")[1];
}

可能你会说,这样的明显有坑的代码, 程序员理所当然会注意, 并对null指针进行校验的. 但事实并非如此, 因为null是一个特殊类型, 它可以表示一切的类型, 所以上面的代码是肯定可以编译通过的. 没有了编译器的约束, 只要使用testNull函数的时候没有查看源码, 或者源码非常复杂, 一下子理不清思路, 防御式编程落实不到, 就会忽略了null, 运行时就有可能抛出NullPointException, 导致程序crash. 这种情况真的防不胜防.

Guava对于空指针的态度

因为上文提到或者隐藏但没提到的种种问题, Guava的诸多类库在设计时就不支持null. 如果检测到null的存在, Guava的类库就会快速失败(fail fast),一般的处理策略是抛出异常. 虽说null存在种种的坑, 但null依旧是Java的一项关键特性, 因此Guava的类库也不能将null彻底拒之门外. 此外, Guava秉承既然不能消灭null, 那就把null建设得更好用的理念, 除了提供了一些工具可以让开发者避免使用null, 还提供了可以让开发者更易于使用null的工具.

Optional

在很多情况下, 程序员使用null是为了表示有些值可能存在或者不存在. 我们又可以用熟悉的Map.get(key)函数来举例, 如果规定null不能作为value值使用(但事实并非如此), 那么当这个函数返回null时就代表没有找到这个key对应的value. 为了应对这种使用null的情况, Guava团队参考其他语言(例如Scala)应对null的实践, 开发了Optional<T>类. Optional类表示那些可能为空的值, 一个Optional类要不包含一个非空的T类型的对象引用(这种情况下, 我们称引用对象是存在的-"present"), 要不什么东西都不包含(这种情况被, 我们说引用对象是不存在的-"absent"), 除此之外, Optional不存在其他情况, 更没有可能是null.

Java8的Optional

鉴于我对Optional类的兴趣, 我用下面这条命令找了一个Guava库Optional开发的最初提交历史:

find guava/ -name "Optional.java" -print | xargs -I '{}' git log --pretty=tformat:%cd-%aN-%s --date=iso |tail -n2
# 结果如下
# 2009-09-15 19:50:59 +0000-kevinb@google.com-Initial code dump: version 9.09.15
# 2009-06-18 18:11:55 +0000-(no author)-Initial directory structure.

从Guava的commit历史中, 我们可以知道Optional最开始是在2009年开始开发的, 而10年前还是Java6的时代, Java7都尚未发布. 在那个"远古年代", 是Guava的Optional一直引领着Java的抗击null重任, 为众多的蒙受"空指针之苦"的Java的程序员带来希望之光. 而当时光的脚步终于来到2014年3月18号, 在这一天, Java程序员迎来了Java8, 这是自Java5发布以来最激动人心的发布. 这天之后, 尘埃落定, Optional, Stream, Lambda等诸多令人期待已久的特性终于成为Java的标准库的一部分, 而这也意味, Guava的Optional已经完成了自己的使命, 成为历史. Guava的Optional类与JDK的Optional功能类似, 既然JDK的Optional已成为正统, 那么下面我就不再介绍Guava的Optional(Guava的wiki本来是有较大篇幅介绍自家的Optional, 个人感觉已经意义不大), 转而介绍JDK的Optional(下文通称为Optional).

Optional构造方式

在使用Optional之前, 首先需要了解如果构造Optional对象, 方式有如下几种:

声明一个空的Optional对象

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

Optional<T> optional = Optional.empty();

根据一个非空值创建Optional

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

Optional<T> optional = Optional.of(objectT);

需要注意的是, 按照Optional的源码声明, 如果传入的objectTnull, 那么Optional就会立刻抛出NullPointException(这就是快速失败-fail fast), 而还是等到访问optional属性时才返回一个错误.

/**
    * Returns an {@code Optional} with the specified present non-null value.
    *
    * @param <T> the class of the value
    * @param value the value to be present, which must be non-null
    * @return an {@code Optional} with the value present
    * @throws NullPointerException if value is null
    */
public static <T> Optional<T> of(T value) {
    return new Optional<>(value);
}

可接受null的Optional

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

Optional<T> optional = Optional.ofNullable(objectT);

如果objectTnull, 那么得到的Optional对象就是个空对象.

Optional的消费方式

Optional与Stream的邂逅

既然Optional在Oracle的文档中被定性为一个容器(container), 那么对于一个容器, 我们关注的点无非是这个容器如何(对于Optional来说是构造)和如何这两件事而已(也就是消费). 在谈Optional的消费接口之前, 先来回顾一下Java8引进的Stream操作(关于Java8 Stream操作的说明已经汗牛充栋了, 既然珠玉在前, 我就不赘言了), 常用的Stream操作函数有如下几个:

  • filter
  • map
  • flatmap
  • peek
  • reduce
  • 更多的函数可以参考Oracle文档

因为前文已经说过Optional是容器类, 那么按理来说, 正常容器类支持的Stream操作, Optional也支持. 只不过在Java8的时候, Optional只支持filter,mapflatmap这三个Stream操作. 可能是因为Java委员会的奆佬们也觉得Optional身为一个容器类只支持三个Stream操作有点丢人, 所以在Java9, Optional增加了一个Optional.stream()这样一个可以返回Stream对象的函数, 让Optional拥有了容器类操作Stream的所有能力, 重振了身为一个容器的荣光. OptionalStream结合使用的示例如下:

public String getCarInsuranceName(Optional<Person> person) {     
    return person.flatMap(Person::getCar)                  
    .filter(car->car.getName().equals("Spaceship"))
    .flatMap(Car::getInsurance)                  
    .map(Insurance::getName)                  
    .orElse("Unknown");

默认行为及解引用Optional对象

除了使用Stream来消费Optional对象, 还可以使用解引用读取Optional实例中的变量值以及定义默认行为, 具体函数说明如下:

  1. get()是这些方法中最简单但又最不安全的方法. 如果变量存在, 它直接返回封闭的变量值. 否则就抛出一个NoSuchElementException异常. 所以, 除非是非常确定Optional变量一定包含值, 否则使用这个函数就相当容易踩坑. 此外, 使用这个函数和直接进行null检查差别并不大.
  2. orElse(T other) 该函数允许在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>)orElseGet函数类似, 可以在变量存在的时候执行传入的函数, 否则就不进行任何操作.

Optional 实战示例

在啰啰嗦嗦介绍了一系列Optional的概念之后, 是时候来看一下Option的实例了. 现存的Java API几乎都是通过返回一个null的方式表示所需的值的缺失, 或者由于某些原因计算无法得到所需的值. 在上文, 我们已经给null盖棺定论了, null是有坑的, 甚至是有害的, 所以要尽量少用null. 而现存的海量Java API都已经使用null作为返回结果, 我们没可能把这些API都重构成返回一个Optional对象的, 但眼看着Optional这样一个设计更完善无法在已有的Java API中使用未免令人心有不甘. 现实中, 可能我们无法修改这些API的签名, 但是我们却可以很轻易地用Optional对象对这些API的返回值进行封装. 现在还是用熟悉的Map举例, 假设有一个Map<String, Object>的对象, 在查询key对应的value时, 如果value不存在, 那么调用Map.get(key)就会返回一个null:

Object value = map.get(key);

现在, 每次使用value都需要进行空指针判断, 着实是太繁琐. 为了解决这个问题, 可以使用Optional.ofNullable函数进行优化:

Map<String, Object> map = new HashMap<>();
map.put("foo", "bar");
String value = Optional.ofNullable(map.get("foo")).map(Object::toString).orElse("helloworld");

这样, 每次使用value都不会再有NullPointException的忧虑.

结语

本文最开始只是想阐述Guava类库使用空指针和避免使用空指针的设计理念, 只是因为Guava大部分类库都是不支持null, 因此使用Guava自家的Optional类来代替null的大部分应用场景, 而Guava自家的Optional无可避免地被JDK的Optional取代, 所以本文大部份的内容也变成对JDK的Optional的探讨. 相信下篇文章会有所改观, 总不可能Guava所有的工具类, 都有JDK对应的竞品, 如果真是这样的话, JDK应该改名为GDK :)

参考