专栏首页Java Tale厌倦了空指针异常?考虑使用Java SE 8的Optional!

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


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

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


说明

一个聪明的人曾经表示,在处理空指针异常之前,你不是一个真正的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类的方法,供参考

序号

方法

描述

1

static Optional empty()

返回空的可选实例。

2

boolean equals(Object,obj)

指示是否一些其他的对象是“等于”这个选项。

3

Optional filter(Predicate<? super predicate)

如果某个值存在,且该值与给定的谓词匹配,则它返回一个可选的描述值,否则返回一个空的可选值。

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 Optional of(T value)

返回一个可选的指定非空值。

11

static Optional ofNullable(T value)

返回一个可选的描述指定值,如果非NULL,否则返回一个空可选。

12

T orElse(T other)

如果目前的返回值,否则返回其他。

13

T orElseGet(Supplier<? extends T> other)

返回当前的值,否则调用其他,并返回该调用的结果。

14

T orElseThrow(Supplier<? extends X> exceptionSupplier)

返回所包含的值,如果存在,则抛出由所提供的供应商创建的异常。

15

String toString()

返回此选项的非空字符串表示,适合调试。

本文由 小马哥 创作,采用 知识共享署名4.0 国际许可协议进行许可 本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名 最后编辑时间为: 2017/11/23 09:19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 一小时教你学会 Maven 项目的构建与管理(3)

    Nexus下载地址:http://www.sonatype.org/nexus/archived/

    IT小马哥
  • String转换成Integer源码分析

    我们经常为用到Integer.valueOf(String str)这个方法,如果字符串格式不对,这个方法会抛出一个系统异常NumberFormatExcept...

    IT小马哥
  • 上传本地代码到github

    点击下面的Create repository,就会进入到类似下面的一个页面,拿到创建的仓库的https地址,红框标示的就是

    IT小马哥
  • 使用 Java 8 Optional 的正确姿势

    我们知道 Java 8 增加了一些很有用的 API, 其中一个就是 Optional. 如果对它不稍假探索, 只是轻描淡写的认为它可以优雅的解决 NullPoi...

    哲洛不闹
  • 探究Java8的Optional 类

    “ 在前面我们已经谈论过Java8的Lambda表达式,方法引用,Stream。除了这些之外Java8中还有一个很重要的知识:Optional ”

    每天学Java
  • Java8新特性之空指针异常的克星Optional类

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

    用户1161110
  • Java8新特性——Optional

    在开发中,我们常常需要对一个引用进行判空以防止空指针异常的出现。Java8引入了Optional类,为的就是优雅地处理判空等问题。现在也有很多类库在使用Opti...

    Happyjava
  • 使用Optional摆脱NPE的折磨

    architectural-architectural-design-architecture

    阿杜
  • Jdk14都要出了,还不能使用 Optional优雅的处理空指针?

    空指针确实会产生很多问题,我们经常遇到空的引用,然后又想从这个空的引用上去获取其他的值,接着理所当然的碰到了 NullPointException。这是你可能会...

    未读代码
  • Java 8中的Optional 类型与 Kotlin 中的可空类型Java 8中的Optional 类型与 Kotlin 中的可空类型Kotlin 中的可空类型《Kotlin极简教程》正式上架:

    其中,我们使用 String? 同样表达了 Optional<String>的意思,相比之下,哪个更简单?

    一个会写诗的程序员

扫码关注云+社区

领取腾讯云代金券