欢迎光临
感受代码之美

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

一个聪明人曾经说过,在处理空指针异常之前,你不是真正的Java程序员。 开玩笑说,空引用是许多问题的根源,因为它通常意味着值不存在。 Java SE 8引入了一个名为java.util.Optional的新类,可以缓解其中的一些问题。

让我们从一个例子开始,看看null的危险性。让我们考虑一个计算机的嵌套对象结构,如图1所示。

图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 对象实际做些什么呢? 毕竟,您想要获得USB端口的版本号。 简而言之,Optional类包括明确处理存在或不存在值的情况的方法。 但是,与null引用相比的优点是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 ,因此调用map方法是完全正确的。 但是,getSoundcard()返回Optional 类型的对象。 这意味着map操作的结果是Optional >类型的对象。 因此,对getUSB()的调用无效,因为最外面的Optional包含另一个Optional作为其值,当然这不支持getUSB()方法。 图3显示了您将获得的嵌套Optional结构。

图三:两层Optional

那么我们如何解决这个问题呢? 同样,我们可以查看您之前可能使用过的流模式:flatMap方法。 对于流,flatMap方法将函数作为参数,返回另一个流。 此函数应用于流的每个元素,它将返回流的流。 但是,flatMap可以用流的内容替换生成的留。 换句话说,由函数生成的所有单独的流被合并或“flattened”为单个流。 我们在这里想要的是类似的东西,但是我们想要将两级可选“flatten”为一个。

好吧,这是个好消息:Optional也支持flatMap方法。 它的目的是将变换函数应用于Optional的值(就像map操作一样),然后将生成的两级Optional压缩为一个。 图4说明了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以处理缺少值; 因此,您可以保护代码免受意外的空指针异常的影响。

原文:
Tired of Null Pointer Exceptions? Consider Using Java SE 8’s Optional!

转载请注明来源:四个空格 » 厌倦了空指针异常?考虑使用Java SE 8的Optional类吧!

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址