厌倦了空指针异常? 考虑使用Java SE 8的Optional!使代码更具可读性并使得免受空指针异常的影响。
有人曾经说过,在未处理空指针异常之前,你不是真正的Java程序员。 开玩笑说,空引用是许多问题的根源,因为它通常表示缺少值。 Java SE 8引入了一个名为java.util.Optional的新类,可以缓解一些这样的问题。
让我们从一个例子开始,看看空指针的危险性。 下面是一个计算机的嵌套对象结构,如图所示:
下面的代码可能会产生什么问题?
String version = computer.getSoundcard().getUSB().getVersion();
这段代码看起来没什么问题呀。但是,好多计算机(比如Raspberry Pi)实际上并没有安装声卡(sound card),那么getSoundcard()得到得结果是什么呢?
那么一般来说是返回空引用表示没有声卡。 不幸的是,getUSB()将尝试返回空引用的USB端口,这将在运行时导致NullPointerException,程序奔溃。 想象一下,如果你的程序在生产环境上运行; 如果程序突然报错,你的客户会有什么反应?
空指针是有一些历史背景,计算机科学巨头Tony Hoare写道: "I call it my billion-dollar mistake. It was the invention of the null reference in 1965. I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement."
怎么做才能防止空指针异常发生呢?你可以通过判断是否为null来防止NullPointerException,如下所示:
例1
String version = "UNKNOWN";
if(computer != null){
Soundcard soundcard = computer.getSoundcard();
if(soundcard != null){
USB usb = soundcard.getUSB();
if(usb != null){
version = usb.getVersion();
}
}
}
但是,通过例1我们看到,代码由于嵌套检查变得非常难看。不幸的是,我们需要很多这样得代码来确保不会发生NullPointerException。 此外,业务逻辑中若含有这些检查,会让我们很是厌烦。 事实上,这些代码也降低我们系统的整体可读性。
此外,嵌套检查也是一个容易出错的过程; 如果你忘记检查一个属性是否为空怎么办? 本文将论证使用null表示空值是一种错误的方法。 我们需要一种更好的方法来表达空值和非空值。
为了给出一些上下文,让我们简要介绍一下其他编程语言提供的解决方式。
其他语言的替代方案是什么?
例如Groovy、C#等语言有一个由"?."表示的安全导航操作符,用来保护出现在属性路径中 null 值。如下所示:
String version = computer?.getSoundcard()?.getUSB()?.getVersion();
在这种情况下,如果computer为null,则变量version 将被赋值为null,或者getSoundcard() 返回null,或者getUSB()返回null。 你不需要编写复杂的嵌套条件来检查是否为null。
此外,Groovy还包括Elvis运算符"?:"(至于为什么叫Elvis运算符,是因为'?:'跟一个叫Elvis的摇滚明星(猫王)的发型很像。),当需要默认值时,使用Elvis运算符会使表达式更简洁。 下面,如果使用安全导航操作符的表达式返回null,则返回默认值"UNKNOWN"; 否则,返回可用的version。如下所示:
String version =
computer?.getSoundcard()?.getUSB()?.getVersion() ?: "UNKNOWN";
其他函数语言(如Haskell和Scala)采用不同的解决方案。 Haskell包含一个Maybe类型,它基本上封装了一个可选值。 Maybe类型的值可以包含给定类型的值,也可以不包含任何值,没有空引用的概念。 Scala有一个名为Option[T]的类似构造来封装类型T值的存在或缺失。然后,你必须使用Option显式检查是否存在值,这强制了"null checking.". 你再也不能“忘记这样做”,因为它是由类型系统强制执行的。
好吧,我们似乎偏离了主题,而且这些听起来都相当抽象。 你现在可能会想,“那么,Java SE 8呢?”
Optional 介绍
Java SE 8引入了一个名为 java.util.Optional<T>的新类,受Haskell和Scala思想的启发。 它是一个封装可选值的类,你可以将Optional视为一个单值容器,可以包含值,也不包含值的(然后将其视为“空” )。如图所示:
接下来,我们可以使用Optional更改一下例1的代码:
例2
public class Computer {
private Optional<Soundcard> soundcard;
public Optional<Soundcard> getSoundcard() { ... }
...
}
public class Soundcard {
private Optional<USB> usb;
public Optional<USB> getUSB() { ... }
}
public class USB{
public String getVersion(){ ... }
}
例2中的代码立即显示计算机可能有声卡,也可能没有声卡(声卡是可选的)。 此外,声卡可以选配USB端口。 这是一种改进,因为这个新模型现在可以清楚地反映出是否允许丢失给定值。 请注意,类似的想法已在诸如Guava等类库中早已提供。
但是我们以用Optional<Soundcard>对象实际做些什么呢? 最终只是想要获得USB端口的版本号。 简而言之,Optional类包括处理存在或不存在值的情况的方法。 但是,与空引用(null)相比的优点是:Optional类强制你在值不存在时考虑该情况。 因此,你能更有效地防止代码中出现不期而至的空指针异常。
值得注意的是,Optional类的意图不是替换空引用。 相反,它的目的是帮助设计更易于理解的API,这样只需读取方法的签名,就能了解该方法是否接受一个Optional类型的值。 这会强制你主动解包Optional以处理空值。
采用Optional模式
废话我们就不多说了; 让我们看看代码吧! 首先我们将探讨如何使用Optional重写典型的空检查模式。 在本文结束时,你将了解如何使用Optional来重写例1中执行多个嵌套空检查,如下所示:
String name = computer.flatMap(Computer::getSoundcard)
.flatMap(Soundcard::getUSB)
.map(USB::getVersion)
.orElse("UNKNOWN");
Note: 确保了解Java SE 8 lambdas和方法引用语法(请参阅Java 8:Lambdas)及其流管道概念(请参阅使用Java SE 8 Streams处理数据)。
创建Optional对象
首先,如何创建Optional对象呢? 有如下几种方法:
- 声明一个空的Optional
Optional<Soundcard> sc = Optional.empty();
- 依据一个非空值创建Optional
SoundCard soundcard = new Soundcard();
Optional<Soundcard> sc = Optional.of(soundcard);
如果soundcard是一个null,这段代码会立即抛出一个NullPointerException,而不是等到你试图访问soundcard 的属性值时才返回一个错误。
- 可接受null的Optional
最后,使用静态工厂方法Optional.ofNullable,你可以创建一个允许null值的Optional 对象:
Optional<Soundcard> sc = Optional.ofNullable(soundcard);
如果Soundcard是null,那么得到的Optional对象就是个空对象。
如果值存在,那么就做点什么吧
现在你有了一个Optional对象,你可以使用可用的方法来显式处理其值,无论值存在与否。 而不是必须进行空检查,如下所示:
SoundCard soundcard = ...;
if(soundcard != null){
System.out.println(soundcard);
}
上诉代码你可以使用* ifPresent()*进行重写,如下所示:
Optional<Soundcard> soundcard = ...;
soundcard.ifPresent(System.out::println);
你无需再进行显式空检查; 它由类型系统强制执行。 如果Optional对象为空,则不会打印任何内容。
你可以使用isPresent()方法来判断Optional对象中是否存在值。此外,还由一个get()方法,。如果变量存在,它直接返回封装的变量 值,否则就抛出一个NoSuchElementException异常。这两个方法组合使用,可以防止异常的发生。如下所示:
if(soundcard.isPresent()){
System.out.println(soundcard.get());
}
但是,这不是Optional的推荐用法(它对嵌套空值检查没有太大改进),还有更好的替代方案,我们将在下面讨论。
默认行为及解引用 Optional 对象
如果返回值为null,通常的处理方式是给定一个默认值。 一般来说,你可以使用三元运算符来实现此目的,如下所示:
Soundcard soundcard =
maybeSoundcard != null ? maybeSoundcard
: new Soundcard("basic_sound_card");
使用Optional对象, 你可以使用orElse()方法重写上面的代码 ,使用这种方式你还可以定义一个默认值,遭遇空的Optional变量时,默认值会作为该方法的调用返回值。如下所示:
Soundcard soundcard = maybeSoundcard.orElse(new Soundcard("defaut"));
同理,你也可以使用orElseThrow()方法,与orElse()方法不同的是,使用orElseThrow()时,当遭遇Optional对象为空时都会抛出一个异常,但是使用orElseThrow你可以定制希 望抛出的异常类型。 如下所示:
Soundcard soundcard =
maybeSoundCard.orElseThrow(IllegalStateException::new);
使用filter 剔除特定的值
你经常需要调用某个对象的方法,查看它的某些属性。比如,你可能需要检查USB端口是否为3.0版本。为了以一种安全的方式进行这些操作,你首先需要确定引用指向的USB 对象是否为null,之后再调用它的getVersion()方法,如下所示:
USB usb = ...;
if(usb != null && "3.0".equals(usb.getVersion())){
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的值置空。
使用map 从 Optional 对象中提取和转换值
从对象中提取信息是一种比较常见的模式。比如,你可能想要从Soundcard 对象中提取USB对象。提取之前,你需要检查Soundcard对象是否为null,然后进一步检查它的version是否正确,你可能会写如下代码:
if(soundcard != null){
USB usb = soundcard.getUSB();
if(usb != null && "3.0".equals(usb.getVersion()){
System.out.println("ok");
}
}
我们可以使用map方法重写这种 "checking for null and extracting" (这里是Soundcard对象)的模式。
Optional<Soundcard> maybeSoundcard= Optional.ofNullable(soundcard); Optional<USB> usb = maybeSoundcard.map(Soundcard::getUSB);
从概念上,这与Stream的map方法相差无几。map操作会将提供的函数应用于流的每个元素。如 果Stream为空,就什么也不做。
Optional类的map方法完全相同:你可以把Optional对象看成一种特殊的集合数据,它至多包含一个元素。如果Optional包含一个值,那函数(这里是提取USB端口的方法引用)就将该值作为参数传递给map,对该值进行转换。如果Optional为空,就什么也不做。
最后,我们可以结合map方法和filter方法重写上面的代码,剔除版本不同于3.0的USB端口:
maybeSoundcard.map(Soundcard::getUSB)
.filter(usb -> "3.0".equals(usb.getVersion())
.ifPresent(() -> System.out.println("ok"));
真棒; 我们的代码开始更接近问题陈述,并且没有重复的嵌套空检查妨碍我们!
使用flatMap 链接 Optional 对象
我们已经使用Optional重构了一些以前的代码,那么我们如何以安全的方式编写以下代码呢?
String version = computer.getSoundcard().getUSB().getVersion();
请注意,这些代码的意思都是从另一个对象中提取一个对象,这正是map方法的用处。 在本文前面,我们更改了model,因此Computer具有Optional<Soundcard>,Soundcard 具有 Optional<USB>,因此我们可以利用map重写之前的代码:
String version = computer.map(Computer::getSoundcard)
.map(Soundcard::getUSB)
.map(USB::getVersion)
.orElse("UNKNOWN");
不幸的是,这段代码无法通过编译。为什么呢?computer是 Optional<Computer>类型的变量, 调用map方法应该没有问题。但getSoundcard() 返回的是一个Optional<Soundcard>类型的对象,这意味着map操作的结果是一个Optional<Optional<Soundcard>>类型的对象。因 此,它对getUSB()的调用是非法的,因为最外层的optional对象包含了另一个optional 对象的值,而它当然不会支持e getUSB()方法。下图说明了你会遭遇的嵌套式optional 结构。
A two-level Optional
所以,我们该如何解决这个问题呢?让我们再回顾一下在流上使用过的模式: flatMap方法。使用流时,flatMap方法接受一个函数作为参数,这个函数的返回值是另一个流。 这个方法会应用到流中的每一个元素,终形成一个新的流的流。但是flagMap会用流的内容替 换每个新生成的流。换句话说,由方法生成的各个流会被合并或者扁平化为一个单一的流。这里你希望的结果其实也是类似的,但是你想要的是将两层的optional合并为一个。
好吧,这是个好消息:Optional也支持flatMap方法。 它的目的是将转换函数应用于Optional的值(就像map操作一样),然后将两层的optional合并为一个。 下图说明了transform函数返回Optional对象时map和flatMap之间的区别。
因此,相信现在你已经对Optional的map和flatMap方法有了一定的了解,让我们看看如何应用。我们需要使用flatMap重写上面的代码,如下所示:
String version = computer.flatMap(Computer::getSoundcard)
.flatMap(Soundcard::getUSB)
.map(USB::getVersion)
.orElse("UNKNOWN");
第一个flatMap确保返回Optional<Soundcard> 对象,而不是Optional<Optional<Soundcard>>,同理,第二个flatMap返回 Optional<USB>对象。 请注意,第三个调用需要map()方法,因为getVersion()返回String对象,而不是Optional对象。
哇! 从编写痛苦的嵌套空检查到编写组合代码,再到可读性强,更有效地防止代码中出现不期而至的空指针异常,我们已经做的越来越好了。
结论
在本文中,我们已经学习了如何采用新的Java SE 8 java.util.Optional<T>。 Optional的目的不是替换代码中的每个空引用,而是帮助设计更好的API,只需读取方法的签名 - 就能了解该方法是否接受一个Optional类型的值。 此外,Optional强制主动解包Optional以处理空值; 因此,可以保护代码免受意外的空指针异常的影响。
相关代码请参见我的github optionalExample
附录:
方 法 | 描 述 |
---|---|
empty | 返回一个空的 Optional 实例 |
filter | 如果值存在并且满足提供的谓词,就返回包含该值的 Optional 对象;否则返回一个空的 Optional 对象 |
flatMap | 如果值存在,就对该值执行提供的 mapping函数调用,返回一个 Optional 类型的值,否则就返 回一个空的 Optional 对象 |
get | 如果该值存在,将该值用 Optional 封装返回,否则抛出一个 NoSuchElementException 异常 |
ifPresent | 如果值存在,就执行使用该值的方法调用,否则什么也不做 |
isPresent | 如果值存在就返回 true,否则返回 false |
map | 如果值存在,就对该值执行提供的 mapping函数调用 |
of | 将指定值用 Optional 封装之后返回,如果该值为 null,则抛出一个 NullPointerException 异常 |
ofNullable | 将指定值用 Optional 封装之后返回,如果该值为 null,则返回一个空的 Optional 对象 |
orElse | 如果有值则将其返回,否则返回一个默认值 |
orElseGet | 如果有值则将其返回,否则返回一个由指定的 Supplier 接口生成的值 |
orElseThrow | 如果有值则将其返回,否则抛出一个由指定的 Supplier 接口生成的异常 |
网友评论