前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >厌倦了空指针异常?考虑使用Java SE 8的Optional!

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

作者头像
IT小马哥
发布2020-03-16 19:03:37
1.3K0
发布2020-03-16 19:03:37
举报
文章被收录于专栏:Java TaleJava Tale

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

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


说明

一个聪明的人曾经表示,在处理空指针异常之前,你不是一个真正的Java程序员。开玩笑,空引用是许多问题的根源,因为它通常用于表示没有值。Java SE 8引入了一个新的类java.util.Optional,可以减轻其中的一些问题。

我们从一个例子开始,看到null的危险。我们来看一个嵌套的对象结构Computer,如图1所示。

图1:用于表示a的嵌套结构 Computer

以下代码可能有问题吗?

代码语言:javascript
复制
String version = computer.getSoundcard().getUSB().getVersion();

这段代码看起来很合理。然而,许多计算机(例如,Raspberry Pi)实际上并不附带声卡。那么结果是getSoundcard()什么呢?

一个常见的(bad)做法是返回null引用以指示没有声卡。不幸的是,这意味着调用getUSB()将尝试返回一个空引用的USB端口,这将导致NullPointerException运行时,并阻止程序进一步运行。想象一下,如果您的程序在客户的机器上运行; 如果程序突然失败,您的客户会说什么? 为了给出一些历史背景,计算机科学巨人托尼·霍尔(Tony Hoare)写道:“我称之为我十亿美元的错误,这是1965年发明的无效参考。我无法抗拒放弃的诱惑一个null引用,只是因为它很容易实现。“

你可以做什么来防止意外的空指针异常?您可以防御并添加检查以防止取消引用,如下列代码所示:

代码语言:javascript
复制
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,但没有将其纳入该版本。)它的工作原理如下:

代码语言:javascript
复制
String version = computer?.getSoundcard()?.getUSB()?.getVersion();

在这种情况下,变量version将被分配为null,如果computer为null,或getSoundcard()返回null,或getUSB()返回null。您不需要编写复杂的嵌套条件来检查null。

此外,Groovy还包括Elvis操作员 “ ?:”(如果您侧身看着,您会认识到Elvis着名的头发),当需要默认值时,可以使用它。在下列情况下,如果使用安全导航运算符的表达式返回null,"UNKNOWN"则返回默认值; 否则返回可用的版本标签。

代码语言:javascript
复制
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() { ... } ... }

代码语言:javascript
复制
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中正在进行多个嵌套空值检查的代码:

代码语言:javascript
复制
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:

代码语言:javascript
复制
Optional<Soundcard> sc = Optional.empty(); 

这里是Optional一个非空值:

代码语言:javascript
复制
SoundCard soundcard = new Soundcard();
Optional<Soundcard> sc = Optional.of(soundcard); 

如果soundcard为null,NullPointerException则会立即抛出一个(而不是在尝试访问该属性时发生潜在错误soundcard)。

另外,通过使用ofNullable,您可以创建一个Optional可能保持空值的对象:

代码语言:javascript
复制
Optional<Soundcard> sc = Optional.ofNullable(soundcard); 

如果Soundcard为空,则生成的Optional对象将为空。

做某事如果价值存在

现在你有一个Optional对象,你可以访问可用的方法来明确地处理值的存在或不存在。而不必记得做一个空检查,如下所示:

代码语言:javascript
复制
SoundCard soundcard = ...;
if(soundcard != null){
  System.out.println(soundcard);
}

您可以使用以下ifPresent()方法:

代码语言:javascript
复制
Optional<Soundcard> soundcard = ...;
soundcard.ifPresent(System.out::println);

您不再需要执行明确的空检查; 它由类型系统执行。如果Optional对象为空,则不会打印任何内容。

您还可以使用该isPresent()方法来确定Optional对象中是否存在值。另外还有一个get()方法返回Optional对象中包含的值,如果它存在的话。否则,它会抛出一个NoSuchElementException。这两种方法可以组合起来,如下,以防止异常:

代码语言:javascript
复制
if(soundcard.isPresent()){
  System.out.println(soundcard.get());
}

然而,这不是推荐使用Optional(对嵌套空检查来说,这不是很大的改进),而且有更多的惯用选择,我们在下面探讨。

默认值和操作

典型的模式是返回默认值,如果确定操作的结果为空。一般来说,您可以使用三元运算符来实现:

Soundcard soundcard = maybeSoundcard != null ? maybeSoundcard : new Soundcard("basic_sound_card");

使用Optional对象,您可以使用orElse()方法重写此代码,该方法提供了一个默认值(如果Optional为空):

代码语言:javascript
复制
Soundcard soundcard = maybeSoundcard.orElse(new Soundcard("defaut"));

类似地,您可以使用该orElseThrow()方法,而不是提供默认值(如果Optional为空)则会引发异常:

代码语言:javascript
复制
Soundcard soundcard = 
  maybeSoundCard.orElseThrow(IllegalStateException::new);

使用filter方法拒绝某些值

通常,您需要调用对象上的方法并检查某些属性。例如,您可能需要检查USB端口是否是特定版本。要以安全的方式执行此操作,您首先需要检查指向USB对象的引用是否为空,然后调用该getVersion()方法,如下所示:

代码语言:javascript
复制
USB usb = ...;
if(usb != null && "3.0".equals(usb.getVersion())){
  System.out.println("ok");
}

可以使用对象filter上的方法重写此模式Optional,如下所示:

代码语言:javascript
复制
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对象,然后进一步检查它是否是正确的版本。你通常会写下面的代码:

代码语言:javascript
复制
if(soundcard != null){
  USB usb = soundcard.getUSB();
  if(usb != null && "3.0".equals(usb.getVersion()){
System.out.println("ok");
  }
}

我们可以Soundcard使用该map方法重写“检查null和提取”(这里是对象)的这种模式。

代码语言:javascript
复制
Optional<USB> usb = maybeSoundcard.map(Soundcard::getUSB);

map与流一起使用的方法是直接平行的。在那里,您将一个函数传递给map方法,该方法将此函数应用于流的每个元素。但是,如果流为空,则不会发生任何事情。

该类的map方法Optional完全相同:内部包含的值Optional通过作为参数传递的函数进行“转换”(这里是提取USB端口的方法引用),而如果Optional为空,则不会发生任何反应。

最后,我们可以将map方法与filter方法结合使用,以拒绝其版本不同于3.0的USB端口:

代码语言:javascript
复制
maybeSoundcard.map(Soundcard::getUSB)
  .filter(usb -> "3.0".equals(usb.getVersion())
  .ifPresent(() -> System.out.println("ok"));

真棒; 我们的代码开始看起来更接近于问题陈述,并且没有详细的null检查方式!

Optional使用flatMap方法级联对象

您已经看到可以重构使用的几种模式Optional。那么我们如何以安全的方式写下面的代码呢?

代码语言:javascript
复制
String version = computer.getSoundcard().getUSB().getVersion();

请注意,所有这些代码都是从另一个提取一个对象,这正是该map方法的一个对象。在文章的前面,我们改变了我们的模型,所以Computer有一个Optional和一个Soundcard有一个Optional,所以我们应该能够写下列内容:

代码语言:javascript
复制
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:

代码语言:javascript
复制
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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017-10-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 说明
  • 没有什么替代品?
  • Optional 简而言之
  • 采用模式 Optional
  • 创建Optional对象
  • 做某事如果价值存在
  • 默认值和操作
  • 使用filter方法拒绝某些值
  • 使用该map方法提取和转换值
  • Optional使用flatMap方法级联对象
  • 结论
  • Optional类使用场景
  • Optional类方法参考
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档