java8-避免空检查

在1965 年有人提出了这个计算机科学中最糟糕的错误,该错误比Windows 的反斜线更加丑陋、比 === 更加怪异、比PHP 更加常见、比CORS 更加不幸、比Java 泛型更加令人失望、比XMLHttpRequest 更加反复无常、比C 预处理器更加难以理解、比MongoDB 更加容易出现碎片问题、比UTF-16 更加令人遗憾。

如何防止Java中著名的NullPointerException问题?这是每个Java初学者迟早会问的关键问题之一。但是中级和专家程序员也绕不开时不时出现的这个错误。它是迄今为止Java和许多其他编程语言中最普遍的错误类型。

图领奖得主和Null-References的发明者Tony Hoare在2009年道歉并将这种错误成为自己的十亿美元错误。

“我把 Null 引用称为自己的十亿美元错误。它的发明是在1965 年,那时我用一个面向对象语言( ALGOL W )设计了第一个全面的引用类型系统。我的目的是确保所有引用的使用都是绝对安全的,编译器会自动进行检查。但是我未能抵御住诱惑,加入了Null引用,仅仅是因为实现起来非常容易。它导致了数不清的错误、漏洞和系统崩溃,可能在之后 40 年中造成了十亿美元的损失。近年来,大家开始使用各种程序分析程序,比如微软的 PREfix 和 PREfast 来检查引用,如果存在为非 Null 的风险时就提出警告。更新的程序设计语言比如 Spec# 已经引入了非 Null 引用的声明。这正是我在1965年拒绝的解决方案。” —— 《Null References: The Billion Dollar Mistake》托尼·霍尔(Tony Hoare),图灵奖得主

无论如何,我们必须处理它。那么我们可以做些什么来防止NullPointerExceptions呢?嗯,显而易见的答案是在所有地方添加空检查。由于空检查有点麻烦和痛苦,许多语言添加了特殊的语法来处理通过空合并运算符进行空检查- 在Groovy或Kotlin等语言中也称为elvis运算符。

不幸的是,Java没有提供这样的语法糖。但幸运的是,在Java Version 8中,事情变得更好了。这篇文章描述了一些技术如何通过利用Java 8的新功能(如lambda表达式)来防止编写不必要的空值检查。

提高Java 8中的空安全性

java8-stream教程一文中已经初步介绍了如何利用Java 8 的Optional类型来防止空值检查。

假设我们有一个像这样的嵌套类结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Outer {
Nested nested;
Nested getNested() {
return nested;
}
}
class Nested {
Inner inner;
Inner getInner() {
return inner;
}
}
class Inner {
String foo;
String getFoo() {
return foo;
}
}

在此结构中解析深层嵌套路径可能有点尴尬。我们必须编写一堆空检查以确保不会引发NullPointerException

1
2
3
4
Outer outer = new Outer();
if (outer != null && outer.nested != null && outer.nested.inner != null) {
System.out.println(outer.nested.inner.foo);
}

我们可以通过使用Java 8 Optional类型来消除所有这些空值检查。该map方法接受一个lambda表达式类型的Function,并自动将每个函数结果包装成一个Optional。这使我们能够以管道式的map进行连续操作。无效检测会在这种模式下自动进行。

1
2
3
4
5
Optional.of(new Outer())
.map(Outer::getNested)
.map(Nested::getInner)
.map(Inner::getFoo)
.ifPresent(System.out::println);

实现相同行为的另一种方法是使用Supplier函数来解析嵌套路径:

1
2
3
Outer obj = new Outer();
resolve(() -> obj.getNested().getInner().getFoo())
.ifPresent(System.out::println);

调用obj.getNested().getInner().getFoo())可能会抛出一个NullPointerException。在这种情况下,将捕获异常并为该方法返回Optional.empty()。

1
2
3
4
5
6
7
8
9
public static <T> Optional<T> resolve(Supplier<T> resolver) {
try {
T result = resolver.get();
return Optional.ofNullable(result);
}
catch (NullPointerException e) {
return Optional.empty();
}
}

请记住,两种解决方案可能都不如传统的空检查那样高效。在大多数情况下,这应该不是什么大问题。

翻译原文: https://winterbe.com/posts/2015/03/15/avoid-null-checks-in-java/