使您的代码更可读,并保护它免受空指针异常。
说明
一个聪明的人曾经表示,在处理空指针异常之前,你不是一个真正的Java程序员。开玩笑,空引用是许多问题的根源,因为它通常用于表示没有值。Java SE 8引入了一个新的类java.util.Optional,可以减轻其中的一些问题。
我们从一个例子开始,看到null的危险。我们来看一个嵌套的对象结构Computer,如图1所示。
图1:用于表示a的嵌套结构 Computer
以下代码可能有问题吗?
String version = computer.getSoundcard().getUSB().getVersion();
这段代码看起来很合理。然而,许多计算机(例如,Raspberry Pi)实际上并不附带声卡。那么结果是getSoundcard()什么呢?
一个常见的(bad)做法是返回null引用以指示没有声卡。不幸的是,这意味着调用getUSB()将尝试返回一个空引用的USB端口,这将导致NullPointerException运行时,并阻止程序进一步运行。想象一下,如果您的程序在客户的机器上运行; 如果程序突然失败,您的客户会说什么? 为了给出一些历史背景,计算机科学巨人托尼·霍尔(Tony Hoare)写道:“我称之为我十亿美元的错误,这是1965年发明的无效参考。我无法抗拒放弃的诱惑一个null引用,只是因为它很容易实现。“
你可以做什么来防止意外的空指针异常?您可以防御并添加检查以防止取消引用,如下列代码所示:
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,但没有将其纳入该版本。)它的工作原理如下: 诸如Groovy之类的语言具有由“ ” 表示的安全导航操作,?.用于安全浏览潜在的空引用。(请注意,它很快被包含在C#中,并且被提出用于Java SE 7,但没有将其纳入该版本。)它的工作原理如下:
String version = computer?.getSoundcard()?.getUSB()?.getVersion();
在这种情况下,变量version将被分配为null,如果computer为null,或getSoundcard()返回null,或getUSB()返回null。您不需要编写复杂的嵌套条件来检查null。
此外,Groovy还包括Elvis操作员 “ ?:”(如果您侧身看着,您会认识到Elvis着名的头发),当需要默认值时,可以使用它。在下列情况下,如果使用安全导航运算符的表达式返回null,"UNKNOWN"则返回默认值; 否则返回可用的版本标签。
String version =
computer?.getSoundcard()?.getUSB()?.getVersion() ?: "UNKNOWN";
其他功能语言,如Haskell和Scala,采取不同的视图。Haskell包括一个Maybe类型,它基本上封装了一个可选的值。类型Maybe的值可以包含给定类型的值或不包含任何值。没有空引用的概念。Scala有一个类似的结构,Option[T]用于封装类型值的存在或不存在T。然后,您必须使用Option类型上可用的操作来显式检查值是否存在,这强加了“空检”的想法。你不能再“忘记这样做”,因为它是由类型系统执行的。
好的,我们分歧了一切,这听起来很抽象。您可能现在想知道,“那么Java SE 8呢?”
Optional 简而言之
Java SE 8引入了一个名为j的新类ava.util.Optional
我们可以更新我们的模型以使用Optional
public class Computer {
private Optional
public Optional
public class Soundcard {
private Optional<USB> usb;
public Optional<USB> getUSB() { ... }
}
public class USB{
public String getVersion(){ ... }
}
代码立即显示计算机可能有也可能没有声卡(声卡是可选的)。此外,声卡可以选择具有USB端口。这是一个改进,因为这个新模型现在可以清楚地反映给定值是否被允许丢失。请注意,类似的想法已经在图书馆,如番石榴。
但是你可以用一个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();
这里是Optional一个非空值:
SoundCard soundcard = new Soundcard();
Optional<Soundcard> sc = Optional.of(soundcard);
如果soundcard为null,NullPointerException则会立即抛出一个(而不是在尝试访问该属性时发生潜在错误soundcard)。
另外,通过使用ofNullable,您可以创建一个Optional可能保持空值的对象:
Optional<Soundcard> sc = Optional.ofNullable(soundcard);
如果Soundcard为空,则生成的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(对嵌套空检查来说,这不是很大的改进),而且有更多的惯用选择,我们在下面探讨。
默认值和操作
典型的模式是返回默认值,如果确定操作的结果为空。一般来说,您可以使用三元运算符来实现:
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对象的引用是否为空,然后调用该getVersion()方法,如下所示:
USB usb = ...;
if(usb != null && "3.0".equals(usb.getVersion())){
System.out.println("ok");
}
可以使用对象filter上的方法重写此模式Optional,如下所示:
Optional<USB> maybeUSB = ...;
maybeUSB.filter(usb -> "3.0".equals(usb.getVersion())
.ifPresent(() -> System.out.println("ok"));
该filter方法使用谓词作为参数。如果一个值存在于Optional对象中,并与谓词匹配,则该filter方法返回该值; 否则返回一个空Optional对象。如果您已经使用filter该Stream接口的方法,您可能已经看到了类似的模式。
使用该map方法提取和转换值
另一种常见的模式是从对象中提取信息。例如,从Soundcard对象中,您可能需要提取USB对象,然后进一步检查它是否是正确的版本。你通常会写下面的代码:
if(soundcard != null){
USB usb = soundcard.getUSB();
if(usb != null && "3.0".equals(usb.getVersion()){
System.out.println("ok");
}
}
我们可以Soundcard使用该map方法重写“检查null和提取”(这里是对象)的这种模式。
Optional<USB> usb = maybeSoundcard.map(Soundcard::getUSB);
map与流一起使用的方法是直接平行的。在那里,您将一个函数传递给map方法,该方法将此函数应用于流的每个元素。但是,如果流为空,则不会发生任何事情。
该类的map方法Optional完全相同:内部包含的值Optional通过作为参数传递的函数进行“转换”(这里是提取USB端口的方法引用),而如果Optional为空,则不会发生任何反应。
最后,我们可以将map方法与filter方法结合使用,以拒绝其版本不同于3.0的USB端口:
maybeSoundcard.map(Soundcard::getUSB)
.filter(usb -> "3.0".equals(usb.getVersion())
.ifPresent(() -> System.out.println("ok"));
真棒; 我们的代码开始看起来更接近于问题陈述,并且没有详细的null检查方式!
Optional使用flatMap方法级联对象
您已经看到可以重构使用的几种模式Optional。那么我们如何以安全的方式写下面的代码呢?
String version = computer.getSoundcard().getUSB().getVersion();
请注意,所有这些代码都是从另一个提取一个对象,这正是该map方法的一个对象。在文章的前面,我们改变了我们的模型,所以Computer有一个Optional
String version = computer.map(Computer::getSoundcard)
.map(Soundcard::getUSB)
.map(USB::getVersion)
.orElse("UNKNOWN");
不幸的是,这段代码没有编译。为什么?可变计算机是类型Optional
好的,这是个好消息:Optional也支持一种flatMap方法。其目的是将变换函数应用于一个值Optional(就像地图操作那样),然后将所得到的两个层次平坦Optional化为一个。图4示出之间的差map和flatMap在变换函数返回一个Optional对象。
图4:使用map与flatMap用Optional
所以,为了使我们的代码正确,我们需要重写如下使用flatMap:
String version = computer.flatMap(Computer::getSoundcard)
.flatMap(Soundcard::getUSB)
.map(USB::getVersion)
.orElse("UNKNOWN");
第一个flatMap确保Optional
哇!我们从编写痛苦的嵌套空白检查到编写能够组合,可读和更好地保护空指针异常的声明性代码已经走了很长的路。
结论
在本文中,我们已经看到了如何采用新的Java SE 8 java.util.Optional
Optional类使用场景
Optional类应该作为可能有返回值函数的返回值类型。有人甚至建议Optional类应该改名为OptionalReturn。 Optional类不是为了避免所有的空指针类型机制。方法或构造函数输入参数强制性检查就仍然是有必要的。 在以下场景一般不建议使用Optional类。
- 领域模型层(非序列化)
- 数据传输对象(同上原因)
- 方法的输入参数
- 构造函数参数
Optional类方法参考
下面摘抄Optional类的方法,供参考
序号 | 方法 | 描述 |
---|---|---|
1 | static | 返回空的可选实例。 |
2 | boolean equals(Object,obj) | 指示是否一些其他的对象是“等于”这个选项。 |
3 | Optional | 如果某个值存在,且该值与给定的谓词匹配,则它返回一个可选的描述值,否则返回一个空的可选值。 |
4 | Optional flatMap(Function<? super T,Optional> mapper) | 如果存在一个值,它将提供的可选轴承映射函数应用到它,返回结果,否则返回空可选。 |
5 | T get() | 如果一个值是可选的,返回值,否则抛出NoSuchElementException。 |
6 | int hashCode() | 返回当前值的哈希代码值(如果有的话),如果没有值,则返回0(0)。 |
7 | void ifPresent(Consumer<? super T> consumer) | 如果存在一个值,它用值调用指定的消费者,否则什么也不做。 |
8 | boolean isPresent() | 如果有一个价值存在返回true,否则为false。 |
9 | Optional map(Function<? super T,? extends U> mapper) | 如果存在一个值,则将所提供的映射函数应用于它,如果结果为非null,则返回一个可选的描述结果。 |
10 | static | 返回一个可选的指定非空值。 |
11 | static | 返回一个可选的描述指定值,如果非NULL,否则返回一个空可选。 |
12 | T orElse(T other) | 如果目前的返回值,否则返回其他。 |
13 | T orElseGet(Supplier<? extends T> other) | 返回当前的值,否则调用其他,并返回该调用的结果。 |
14 | 返回所包含的值,如果存在,则抛出由所提供的供应商创建的异常。 | |
15 | String toString() | 返回此选项的非空字符串表示,适合调试。 |
本文由 小马哥 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为:
2017/11/23 09:19