厌倦了空指针异常?考虑使用Java SE 8的Optional!

/ 语言 / 没有评论 / 1613浏览

使您的代码更可读,并保护它免受空指针异常。

-----------------来自小马哥的故事

说明

一个聪明的人曾经表示,在处理空指针异常之前,你不是一个真正的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,它来自Haskell和Scala的想法。它是一个封装可选值的类,如下面的清单2和图1所示。您可以将其Optional视为包含值或不包含值的单值容器(它被称为“空”) ,如图2所示。

我们可以更新我们的模型以使用Optional public class Computer { private Optional soundcard;
public Optional getSoundcard() { ... } ... }

public class Soundcard {
  private Optional<USB> usb;
  public Optional<USB> getUSB() { ... }

}

public class USB{
  public String getVersion(){ ... }
}

代码立即显示计算机可能有也可能没有声卡(声卡是可选的)。此外,声卡可以选择具有USB端口。这是一个改进,因为这个新模型现在可以清楚地反映给定值是否被允许丢失。请注意,类似的想法已经在图书馆,如番石榴。

但是你可以用一个Optional对象来做什么呢?毕竟,你想要获得USB端口的版本号。简而言之,Optional该类包括明确处理值存在或不存在的情况的方法。然而,与空引用相比的优点是,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和一个Soundcard有一个Optional,所以我们应该能够写下列内容:

String version = computer.map(Computer::getSoundcard)
  .map(Soundcard::getUSB)
  .map(USB::getVersion)
  .orElse("UNKNOWN");

不幸的是,这段代码没有编译。为什么?可变计算机是类型Optional,所以调用该map方法是完全正确的。但是,getSoundcard()返回一个类型的对象Optional。这意味着地图操作的结果是类型的对象Optional<Optional>。结果,调用getUSB()是无效的,因为最外层Optional包含其值Optional,当然不支持该getUSB()方法。图3说明了Optional您将获得的嵌套结构。 那么我们如何解决这个问题呢?再次,我们可以看一下以前使用stream的方式:flatMap方法。使用流,该flatMap方法将一个函数作为参数,返回另一个流。该功能应用于流的每个元素,这将导致流的流。然而,flatMap具有通过该流的内容替换每个生成的流的效果。换句话说,由函数生成的所有单独的流被合并或“扁平化”成一个流。我们在这里想要的是类似的东西,但是我们希望将两层平铺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返回一个而不是一个Optional<Optional>,而第二个flatMap实现相同的目的来返回Optional。请注意,第三个调用只需要一个,map()因为getVersion()返回一个String而不是一个Optional对象。

哇!我们从编写痛苦的嵌套空白检查到编写能够组合,可读和更好地保护空指针异常的声明性代码已经走了很长的路。

结论

在本文中,我们已经看到了如何采用新的Java SE 8 java.util.Optional。目的Optional不是替换代码库中的每一个空引用,而是帮助设计更好的API - 只要读取方法的签名,用户就可以判断是否期望可选的值。另外,Optional迫使你主动展开一个Optional处理没有价值的东西; 因此,您可以保护您的代码免受意外的空指针异常。

Optional类使用场景

Optional类应该作为可能有返回值函数的返回值类型。有人甚至建议Optional类应该改名为OptionalReturn。 Optional类不是为了避免所有的空指针类型机制。方法或构造函数输入参数强制性检查就仍然是有必要的。 在以下场景一般不建议使用Optional类。

Optional类方法参考

下面摘抄Optional类的方法,供参考

序号方法描述
1static Optional empty()返回空的可选实例。
2boolean equals(Object,obj)指示是否一些其他的对象是“等于”这个选项。
3Optional filter(Predicate<? super predicate)如果某个值存在,且该值与给定的谓词匹配,则它返回一个可选的描述值,否则返回一个空的可选值。
4 Optional flatMap(Function<? super T,Optional> mapper)如果存在一个值,它将提供的可选轴承映射函数应用到它,返回结果,否则返回空可选。
5T get()如果一个值是可选的,返回值,否则抛出NoSuchElementException。
6int hashCode()返回当前值的哈希代码值(如果有的话),如果没有值,则返回0(0)。
7void ifPresent(Consumer<? super T> consumer)如果存在一个值,它用值调用指定的消费者,否则什么也不做。
8boolean isPresent()如果有一个价值存在返回true,否则为false。
9Optional map(Function<? super T,? extends U> mapper)如果存在一个值,则将所提供的映射函数应用于它,如果结果为非null,则返回一个可选的描述结果。
10static Optional of(T value)返回一个可选的指定非空值。
11static Optional ofNullable(T value)返回一个可选的描述指定值,如果非NULL,否则返回一个空可选。
12T orElse(T other)如果目前的返回值,否则返回其他。
13T orElseGet(Supplier<? extends T> other)返回当前的值,否则调用其他,并返回该调用的结果。
14 T orElseThrow(Supplier<? extends X> exceptionSupplier)返回所包含的值,如果存在,则抛出由所提供的供应商创建的异常。
15String toString()返回此选项的非空字符串表示,适合调试。