1 前言
To be, or not to be, that is the question:
先来看看奆佬们关于空指针的看法:
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
自然是有其原由的, 不会无的放矢.
那具体原因是什么呢?待下文为你细细道来;
2 空指针的问题
2.1 空指针语意隐晦不明
null
的语意并不了然明确, 即当一个函数返回null
, 我们并不知道null
的意思是指返回结果理应为空? 还是指函数没有达到预期结果, 返回null
表示失败?
举个常见的例子, 当调用Map.get(key)
获取key对应的value的时候, 返回结果为null
; null
是指找不到这个key对应的value?
还是说这个key对应的value本身就是null
, 原来是通过Map.put(key,value)
赋值的呢? null
甚至可以是代表其他东西!
老实说, 当我们获得一个null
, 我们并不清楚它究竟指的是啥,
除非有对应的javadoc 进行了说明.
2.2 空指针”暗藏杀机”
null
除了语意不明外, 还非常容易在不经意间挖坑坑人. 例如有下面的代码:
|
|
可能你会说,这样的明显有坑的代码, 程序员理所当然会注意,
并对null
指针进行校验的.
但事实并非如此, 因为null
是一个特殊类型,
它可以表示一切的类型, 所以上面的代码是肯定可以编译通过的.
没有了编译器的约束, 只要使用testNull
函数的时候没有查看源码,
或者源码非常复杂, 一下子理不清思路, 防御式编程落实不到,
就会忽略了null
, 运行时就有可能抛出NullPointException
, 导致程序crash.
这种情况真的防不胜防.
3 Guava对于空指针的态度
因为上文提到或者隐藏但没提到的种种问题,
Guava的诸多类库在设计时就不支持null
.
如果检测到null
的存在, Guava的类库就会快速失败(fail fast),一般的处理策略是抛出异常.
虽说null
存在种种的坑, 但null
依旧是Java的一项关键特性,
因此Guava的类库也不能将null
彻底拒之门外.
此外, Guava秉承既然不能消灭null
, 那就把null
建设得更好用的理念,
除了提供了一些工具可以让开发者避免使用null
,
还提供了可以让开发者更易于使用null
的工具.
4 Optional
在很多情况下, 程序员使用null
是为了表示有些值可能存在或者不存在.
我们又可以用熟悉的Map.get(key)
函数来举例,
如果规定null
不能作为value
值使用(但事实并非如此),
那么当这个函数返回null
时就代表没有找到这个key
对应的value
.
为了应对这种使用null
的情况, Guava团队参考其他语言(例如Scala)应对null
的实践, 开发了Optional<T>
类.
Optional
类表示那些可能为空的值, 一个Optional
类要不包含一个非空的T
类型的对象引用(这种情况下,
我们称引用对象是存在的-“present”), 要不什么东西都不包含(这种情况被, 我们说引用对象是不存在的-“absent”), 除此之外, Optional
不存在其他情况, 更没有可能是null
.
4.1 Java8的Optional
鉴于我对Optional
类的兴趣,
我用下面这条命令找了一个Guava库Optional
开发的最初提交历史:
|
|
从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
).
4.2 Optional构造方式
在使用Optional
之前, 首先需要了解如果构造Optional
对象,
方式有如下几种:
4.2.1 声明一个空的Optional
对象
可以通过静态工厂方法Optional.empty
, 创建一个空的Optional
对象:
|
|
4.2.2 根据一个非空值创建Optional
还可以使用静态工厂方法Optional.of
,
依据一个非空值创建一个Optional
对象:
|
|
需要注意的是, 按照Optional
的源码声明, 如果传入的objectT
为null
,
那么Optional
就会立刻抛出NullPointException=(这就是快速失败-fail fast), 而还是等到访问=optional
属性时才返回一个错误.
|
|
4.2.3 可接受null的Optional
最后, 使用静态工厂方法Optional.ofNullable
,
我们可以创建一个允许null
的Optional
的对象:
|
|
如果objectT
为null
, 那么得到的Optional
对象就是个空对象.
4.3 Optional的消费方式
4.3.1 Optional与Stream的邂逅
既然Optional
在Oracle的文档中被定性为一个容器(container),
那么对于一个容器,
我们关注的点无非是这个容器如何存*(对于Optional
来说是构造)和如何取*这两件事而已(也就是消费).
在谈Optional
的消费接口之前,
先来回顾一下Java8引进的Stream
操作(关于Java8
Stream
操作的说明已经汗牛充栋了, 既然珠玉在前, 我就不赘言了),
常用的Stream
操作函数有如下几个:
- filter
- map
- flatmap
- peek
- reduce
- 更多的函数可以参考Oracle文档
因为前文已经说过Optional
是容器类, 那么按理来说,
正常容器类支持的Stream
操作, Optional
也支持.
只不过在Java8的时候, Optional
只支持filter
,=map=和flatmap
这三个Stream
操作.
可能是因为Java委员会的奆佬们也觉得Optional
身为一个容器类只支持三个Stream
操作有点丢人,
所以在Java9,
Optional
增加了一个Optional.stream()
这样一个可以返回Stream
对象的函数,
让Optional
拥有了容器类操作Stream
的所有能力,
重振了身为一个容器的荣光. Optional
与Stream
结合使用的示例如下:
|
|
4.3.2 默认行为及解引用Optional对象
除了使用Stream
来消费Optional
对象,
还可以使用解引用读取Optional
实例中的变量值以及定义默认行为,
具体函数说明如下:
get()
是这些方法中最简单但又最不安全的方法. 如果变量存在, 它直接返回封闭的变量值. 否则就抛出一个NoSuchElementException
异常. 所以, 除非是非常确定Optional
变量一定包含值, 否则使用这个函数就相当容易踩坑. 此外, 使用这个函数和直接进行null
检查差别并不大.orElse(T other)
该函数允许在Optional
对象不存在的时候提供一个默认值(也是我个人最常用的使用方式之一)orElseGet(Supplier<? extends T> other)
是orElse
函数的延迟调用版,Supplier
方法只有在Optional
对象不含值的时候才执行. 如果创建默认值是件耗时操作, 那么可以使用这种方式来提升性能, 又或者某个函数仅在Optional
为空的时候才调用, 也可以使用这种方式orElseThrow(Supplier<? extends X> exceptionSupplier)
和get
方法非常类似, 这两个函数都会在Optional
对象为空时, 抛出异常, 但差别在于orElseThrow
可以指定抛出的异常类型ifPresent(Consumer<? super T>)
和orElseGet
函数类似, 可以在变量存在的时候执行传入的函数, 否则就不进行任何操作.
4.3.3 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
:
|
|
现在, 每次使用value
都需要进行空指针判断, 着实是太繁琐.
为了解决这个问题, 可以使用Optional.ofNullable
函数进行优化:
|
|
这样, 每次使用value
都不会再有NullPointException
的忧虑.
5 结语
本文最开始只是想阐述Guava类库使用空指针和避免使用空指针的设计理念,
只是因为Guava大部分类库都是不支持null
,
因此使用Guava自家的Optional
类来代替null
的大部分应用场景,
而Guava自家的Optional
无可避免地被JDK的Optional
取代,
所以本文大部份的内容也变成对JDK的Optional
的探讨.
相信下篇文章会有所改观, 总不可能Guava所有的工具类, 都有JDK对应的竞品,
如果真是这样的话, JDK应该改名为GDK :)