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