一个聪明人曾经说过,在处理空指针异常之前,你不是真正的Java程序员。 开玩笑说,空引用是许多问题的根源,因为它通常意味着值不存在。 Java SE 8引入了一个名为java.util.Optional的新类,可以缓解其中的一些问题。
让我们从一个例子开始,看看null的危险性。让我们考虑一个计算机的嵌套对象结构,如图1所示。
以下代码可能有什么问题?
String version = computer.getSoundcard().getUSB().getVersion();
这段代码看起来很合理。但是,许多计算机(例如,Raspberry Pi)实际上并没有声卡。那么getSoundcard()的结果是什么?
常见的(不友好的)做法是返回空引用以指示没有声卡。 不幸的是,这意味着对getUSB()的调用将尝试返回空引用的USB端口,这将在运行时导致NullPointerException并阻止程序进一步运行。 想象一下,如果您的程序在客户的机器上运行; 如果程序突然失败,您的客户会说什么?
为了给出一些历史背景,计算机科学巨头托尼霍尔写道:“我称之为十亿美元的错误。就是1965年发明的空指针。我无法抗拒使用一个空引用的诱惑,只是因为它很容易实现。“
你能做些什么来防止意外的空指针异常?您可以采取防御措施并添加检查以防止空取消引用,如清单1所示:
代码清单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来表示值不存在是一种错误的方法。 我们需要的是一种更好的方法来模拟值的存在和不存在。
为了给出一些上下文,让我们简要介绍一下其他编程语言提供的内容。
对于Null有没有替代方案?
Groovy等语言有一个由?.
表示的安全导航操作符,用以安全地浏览潜在的空引用。 (请注意,它很快也将被采用到C#中,并且它被提议用于Java SE 7,但没有进入该版本。)它的工作原理如下:
String version = computer?.getSoundcard()?.getUSB()?.getVersion();
在这种情况下,如果computer为null,则变量版本将被赋值为null,或者getSoundcard()返回null,或者getUSB()返回null。您不需要编写复杂的嵌套条件来检查null。
此外,Groovy还包括猫王操作符?:
(如果你侧身看,你会发现酷似猫王的发型),当需要默认值时,它可以用于简单的情况。 在下面的代码中,如果使用安全导航操作符的表达式返回null,则返回默认值“UNKNOWN”; 否则,返回可用的版本标记。
String version = computer?.getSoundcard()?.getUSB()?.getVersion() ?: "UNKNOWN";
其他函数语言(如Haskell和Scala)采用不同的视图。 Haskell包含一个Maybe类型,它基本上封装了一个可选值。 Maybe类型的值可以包含给定类型的值,也可以不包含任何值。 没有空引用的概念。 Scala有一个名为Option [T]的类似构造来封装类型T值的存在或不存在。然后,您必须使用Option类型上可用的操作显式检查是否存在值,这强制了“非空检查”,您不会再“忘记做了”,因为它是由类型检查系统强制执行的。
好吧,别扯远了,这些听起来有点抽象。那么你可能好奇,“Java 8会怎样呢?”
Optional概述
Java SE 8引入了一个名为java.util.Optional <T>
的新类,它受到Haskell和Scala思想的启发。 它是一个封装可选值的类,如下面的清单2和图1所示。您可以将Optional视为包含值或不包含值的单值容器(然后将其视为“空” ),如图2所示。
我们可以更新我们的模型以使用Optional,如清单2所示:
代码清单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
值得注意的是,Optional类的意图不是替换每个空引用。 相反,它的目的是帮助设计更易于理解的API,这样只需读取方法的签名,就可以判断出是否可以获得可选值。 这会强制您主动解包Optional以处理缺少值。
采用Optional的模式
话不多说 让我们看一些代码吧! 我们将首先探讨如何使用Optional重写典型的空检查模式。 在本文结束时,您将了解如何使用Optional(如下所示)来重写清单1中执行多个嵌套空检查的代码:
String name = computer.flatMap(Computer::getSoundcard)
.flatMap(Soundcard::getUSB)
.map(USB::getVersion)
.orElse("UNKNOWN");
注意:确保您了解Java SE 8 lambdas和方法引用语法(请参阅“Java 8:Lambdas”)及其流管道概念(请参阅“使用Java SE 8 Streams处理数据”)。
创建Optional对象
首先,如何创建Optional对象?有几种方法:
这是一个空的Optional:
Optional<Soundcard> sc = Optional.empty();
这是一个带有非null值的Optional:
SoundCard soundcard = new Soundcard();
Optional<Soundcard> sc = Optional.of(soundcard);
如果soundcard为null,则会立即抛出NullPointerException(而不是在尝试访问声卡的属性时获得潜在错误)。
此外,通过使用ofNullable
,您可以创建一个可能包含空值的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()方法返回Optional对象中包含的值(如果存在)。 否则,它会抛出NoSuchElementException。 可以将这两种方法组合起来,以防止例外:
if(soundcard.isPresent()){
System.out.println(soundcard.get());
}
但是,这不是Optional的推荐用法(它对嵌套空值检查没有太大改进),还有更多惯用的替代方案,我们将在下面讨论。
默认值以及操作
如果确定操作的结果为null,则典型模式是返回默认值。通常,您可以使用三元运算符(如下所示)来实现此目的:
Soundcard soundcard =
maybeSoundcard != null ? maybeSoundcard
: new Soundcard("basic_sound_card");
使用Optional对象,可以使用orElse()方法重写此代码,如果Optional为空,则提供默认值:
Soundcard soundcard = maybeSoundcard.orElse(new Soundcard("defaut"));
同样,您可以使用orElseThrow()方法,如果Optional为空,则不会提供默认值,而是抛出异常:
Soundcard soundcard =
maybeSoundCard.orElseThrow(IllegalStateException::new);
使用过滤器方法filter拒绝某些值
通常,您需要在对象上调用方法并检查某些属性。 例如,您可能需要检查USB端口是否为特定版本。 要以安全的方式执行此操作,首先需要检查指向USB对象的引用是否为null,然后调用getVersion()
法,如下所示:
USB usb = ...;
if(usb != null && "3.0".equals(usb.getVersion())){
System.out.println("ok");
}
可以使用Optional对象上的filter方法重写此模式,如下所示:
Optional<USB> maybeUSB = ...;
maybeUSB.filter(usb -> "3.0".equals(usb.getVersion())
.ifPresent(() -> System.out.println("ok"));
filter方法将表达式作为参数。 如果Optional对象中存在一个值并且它与表达式匹配,则filter方法返回该值; 否则,它返回一个空的Optional对象。 如果您已将过滤器方法与Stream接口一起使用,则可能已经看到过类似的模式。
使用map方法提取和转换值
另一种常见模式是从对象中提取信息。 例如,从Soundcard对象中,您可能希望提取USB对象,然后进一步检查它是否是正确的版本。 您通常会编写以下代码:
if(soundcard != null){
USB usb = soundcard.getUSB();
if(usb != null && "3.0".equals(usb.getVersion()){
System.out.println("ok");
}
}
我们可以使用map方法重写这种“检查null和提取”(这里是Soundcard对象)的模式。
Optional<USB> usb = maybeSoundcard.map(Soundcard::getUSB);
与流一起使用的map方法可以进行并行处理。所以,您将一个函数传递给map方法,该方法将此函数应用于流的每个元素。但是,如果流为空,则不会发生任何事情。
Optional类的map方法完全相同:Optional中包含的值由作为参数传递的函数“转换”(这里是提取USB端口的方法引用),而如果Optional为空则没有任何反应。
最后,我们可以结合map方法和filter方法来拒绝版本不同于3.0的USB端口:
maybeSoundcard.map(Soundcard::getUSB)
.filter(usb -> "3.0".equals(usb.getVersion())
.ifPresent(() -> System.out.println("ok"));
真棒;我们的代码开始更接近问题陈述,并且没有累赘的空检查妨碍我们!
使用flatMap方法级联可选对象
您已经看到一些可以重构的模式以使用Optional。那么我们如何以安全的方式编写以下代码呢?
String version = computer.getSoundcard().getUSB().getVersion();
请注意,所有这些代码都是从另一个对象中提取一个对象,这正是map方法的用途。 在本文前面,我们更改了模型,因此计算机具有可选<声卡>,声卡具有可选
String version = computer.map(Computer::getSoundcard)
.map(Soundcard::getUSB)
.map(USB::getVersion)
.orElse("UNKNOWN");
不幸的是,这段代码无法编译。 为什么? 变量计算机的类型为Optional
那么我们如何解决这个问题呢? 同样,我们可以查看您之前可能使用过的流模式:flatMap方法。 对于流,flatMap方法将函数作为参数,返回另一个流。 此函数应用于流的每个元素,它将返回流的流。 但是,flatMap可以用流的内容替换生成的留。 换句话说,由函数生成的所有单独的流被合并或“flattened”为单个流。 我们在这里想要的是类似的东西,但是我们想要将两级可选“flatten”为一个。
好吧,这是个好消息:Optional也支持flatMap方法。 它的目的是将变换函数应用于Optional的值(就像map操作一样),然后将生成的两级Optional压缩为一个。 图4说明了transform函数返回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以处理缺少值; 因此,您可以保护代码免受意外的空指针异常的影响。
原文:
Tired of Null Pointer Exceptions? Consider Using Java SE 8’s Optional!