java8 -Optional

厌倦了空指针异常? 考虑使用Java SE 8的Optional!使代码更具可读性并使得免受空指针异常的影响。
有人曾经说过,在未处理空指针异常之前,你不是真正的Java程序员。 开玩笑说,空引用是许多问题的根源,因为它通常表示缺少值。 Java SE 8引入了一个名为java.util.Optional的新类,可以缓解一些这样的问题。
让我们从一个例子开始,看看空指针的危险性。 下面是一个计算机的嵌套对象结构,如图所示:
嵌套对象结构
下面的代码可能会产生什么问题?

String version = computer.getSoundcard().getUSB().getVersion();

这段代码看起来没什么问题呀。但是,好多计算机(比如Raspberry Pi)实际上并没有安装声卡(sound card),那么*getSoundcard()*得到得结果是什么呢?
那么一般来说是返回空引用表示没有声卡。 不幸的是,getUSB()将尝试返回空引用的USB端口,这将在运行时导致NullPointerException,程序奔溃。 想象一下,如果你的程序在生产环境上运行; 如果程序突然报错,你的客户会有什么反应?
空指针是有一些历史背景,计算机科学巨头Tony Hoare写道: “I call it my billion-dollar mistake. It was the invention of the null reference in 1965. I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement.”
怎么做才能防止空指针异常发生呢?你可以通过判断是否为null来防止
NullPointerException
,如下所示:

例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表示空值是一种错误的方法。 我们需要一种更好的方法来表达空值和非空值。
为了给出一些上下文,让我们简要介绍一下其他编程语言提供的解决方式。

其他语言的替代方案是什么?

例如Groovy、C#等语言有一个由*"?."*表示的安全导航操作符,用来保护出现在属性路径中 null 值。如下所示:

String version = computer?.getSoundcard()?.getUSB()?.getVersion();

在这种情况下,如果computer为null,则变量version 将被赋值为null,或者getSoundcard() 返回null,或者getUSB()返回null。 你不需要编写复杂的嵌套条件来检查是否为null。
此外,Groovy还包括Elvis运算符"?:"(至于为什么叫Elvis运算符,是因为’?:'跟一个叫Elvis的摇滚明星(猫王)的发型很像。),当需要默认值时,使用Elvis运算符会使表达式更简洁。 下面,如果使用安全导航操作符的表达式返回null,则返回默认值"UNKNOWN"; 否则,返回可用的
version
。如下所示:

String version = 
    computer?.getSoundcard()?.getUSB()?.getVersion() ?: "UNKNOWN";

其他函数语言(如Haskell和Scala)采用不同的解决方案。 Haskell包含一个Maybe类型,它基本上封装了一个可选值。 Maybe类型的值可以包含给定类型的值,也可以不包含任何值,没有空引用的概念。 Scala有一个名为Option[T]的类似构造来封装类型T值的存在或缺失。然后,你必须使用Option显式检查是否存在值,这强制了"null checking.". 你再也不能“忘记这样做”,因为它是由类型系统强制执行的。
好吧,我们似乎偏离了主题,而且这些听起来都相当抽象。 你现在可能会想,“那么,Java SE 8呢?”

Optional 介绍

Java SE 8引入了一个名为 java.util.Optional的新类,受Haskell和Scala思想的启发。 它是一个封装可选值的类,你可以将Optional视为一个单值容器,可以包含值,也不包含值的(然后将其视为“空” )。如图所示:
An optional sound card
接下来,我们可以使用Optional更改一下例1的代码:

例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重写典型的空检查模式。 在本文结束时,你将了解如何使用Optional来重写例1中执行多个嵌套空检查,如下所示:

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

Note: 确保了解Java SE 8 lambdas和方法引用语法(请参阅Java 8:Lambdas)及其流管道概念(请参阅使用Java SE 8 Streams处理数据)。

创建Optional对象

首先,如何创建Optional对象呢? 有如下几种方法:

  1. 声明一个空的Optional
Optional<Soundcard> sc = Optional.empty();
  1. 依据一个非空值创建Optional
SoundCard soundcard = new Soundcard();
Optional<Soundcard> sc = Optional.of(soundcard); 

如果soundcard是一个null,这段代码会立即抛出一个NullPointerException,而不是等到你试图访问soundcard 的属性值时才返回一个错误。
3. 可接受null的Optional
最后,使用静态工厂方法Optional.ofNullable,你可以创建一个允许null值的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()*方法,。如果变量存在,它直接返回封装的变量 值,否则就抛出一个NoSuchElementException异常。这两个方法组合使用,可以防止异常的发生。如下所示:

if(soundcard.isPresent()){
  System.out.println(soundcard.get());
}

但是,这不是Optional的推荐用法(它对嵌套空值检查没有太大改进),还有更好的替代方案,我们将在下面讨论。

默认行为及解引用 Optional 对象

如果返回值为null,通常的处理方式是给定一个默认值。 一般来说,你可以使用三元运算符来实现此目的,如下所示:

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

使用Optional对象, 你可以使用*orElse()*方法重写上面的代码 ,使用这种方式你还可以定义一个默认值,遭遇空的Optional变量时,默认值会作为该方法的调用返回值。如下所示:

Soundcard soundcard = maybeSoundcard.orElse(new Soundcard("defaut"));

同理,你也可以使用*orElseThrow()方法,与orElse()方法不同的是,使用orElseThrow()*时,当遭遇Optional对象为空时都会抛出一个异常,但是使用orElseThrow你可以定制希 望抛出的异常类型。 如下所示:

Soundcard soundcard = 
  maybeSoundCard.orElseThrow(IllegalStateException::new);

使用filter 剔除特定的值

你经常需要调用某个对象的方法,查看它的某些属性。比如,你可能需要检查USB端口是否为3.0版本。为了以一种安全的方式进行这些操作,你首先需要确定引用指向的USB 对象是否为null,之后再调用它的*getVersion()*方法,如下所示:

USB usb = ...;
if(usb != null && "3.0".equals(usb.getVersion())){
  System.out.println("ok");
}

使用Optional对象的filter方法,这段代码可以重构如下:

Optional<Insurance> optInsurance = ...; optInsurance.filter(insurance ->                         "CambridgeInsurance".equals(insurance.getName()))             .ifPresent(x -> System.out.println("ok"));

filter方法接受一个谓词作为参数。如果Optional对象的值存在,并且它符合谓词的条件, filter方法就返回其值;否则它就返回一个空的Optional对象。你可以将 Optional看成包含一个元素的Stream对象,这个方法的行为就非常清晰了。如果Optional 对象为空,它不做任何操作,反之,它就对Optional对象中包含的值施加谓词操作。如果该操 作的结果为true,它不做任何改变,直接返回该Optional对象,否则就将该值过滤掉,将 Optional的值置空。

使用map 从 Optional 对象中提取和转换值

从对象中提取信息是一种比较常见的模式。比如,你可能想要从Soundcard 对象中提取USB对象。提取之前,你需要检查Soundcard对象是否为null,然后进一步检查它的version是否正确,你可能会写如下代码:

if(soundcard != null){
  USB usb = soundcard.getUSB();
  if(usb != null && "3.0".equals(usb.getVersion()){
    System.out.println("ok");
  }
}

我们可以使用map方法重写这种 “checking for null and extracting” (这里是Soundcard对象)的模式。

Optional<Soundcard> maybeSoundcard= Optional.ofNullable(soundcard); Optional<USB> usb = maybeSoundcard.map(Soundcard::getUSB);

从概念上,这与Stream的map方法相差无几。map操作会将提供的函数应用于流的每个元素。如 果Stream为空,就什么也不做。
Optional类的map方法完全相同:你可以把Optional对象看成一种特殊的集合数据,它至多包含一个元素。如果Optional包含一个值,那函数(这里是提取USB端口的方法引用)就将该值作为参数传递给map,对该值进行转换。如果Optional为空,就什么也不做。
最后,我们可以结合map方法和filter方法重写上面的代码,剔除版本不同于3.0的USB端口:

maybeSoundcard.map(Soundcard::getUSB)
      .filter(usb -> "3.0".equals(usb.getVersion())
      .ifPresent(() -> System.out.println("ok"));

真棒; 我们的代码开始更接近问题陈述,并且没有重复的嵌套空检查妨碍我们!

使用flatMap 链接 Optional 对象

我们已经使用Optional重构了一些以前的代码,那么我们如何以安全的方式编写以下代码呢?

String version = computer.getSoundcard().getUSB().getVersion();

请注意,这些代码的意思都是从另一个对象中提取一个对象,这正是map方法的用处。 在本文前面,我们更改了model,因此Computer具有Optional,Soundcard 具有 Optional,因此我们可以利用map重写之前的代码:

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

不幸的是,这段代码无法通过编译。为什么呢?computer是 Optional类型的变量, 调用map方法应该没有问题。但getSoundcard() 返回的是一个Optional类型的对象,这意味着map操作的结果是一个Optional<Optional>类型的对象。因 此,它对getUSB()的调用是非法的,因为最外层的optional对象包含了另一个optional 对象的值,而它当然不会支持e getUSB()方法。下图说明了你会遭遇的嵌套式optional 结构。
A two-level Optional

所以,我们该如何解决这个问题呢?让我们再回顾一下在流上使用过的模式: flatMap方法。使用流时,flatMap方法接受一个函数作为参数,这个函数的返回值是另一个流。 这个方法会应用到流中的每一个元素,终形成一个新的流的流。但是flagMap会用流的内容替 换每个新生成的流。换句话说,由方法生成的各个流会被合并或者扁平化为一个单一的流。这里你希望的结果其实也是类似的,但是你想要的是将两层的optional合并为一个。
好吧,这是个好消息:Optional也支持flatMap方法。 它的目的是将转换函数应用于Optional的值(就像map操作一样),然后将两层的optional合并为一个。 下图说明了transform函数返回Optional对象时mapflatMap之间的区别。
Using map versus flatMap with Optional

因此,相信现在你已经对Optional的map和flatMap方法有了一定的了解,让我们看看如何应用。我们需要使用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以处理空值; 因此,可以保护代码免受意外的空指针异常的影响。
相关代码请参见我的github optionalExample

附录:

O p t i o n a l 类 的 方 法 \color{#654321}{\Large\mathbf{Optional类的方法 }} Optional

方 法描 述
empty返回一个空的 Optional 实例
filter如果值存在并且满足提供的谓词,就返回包含该值的 Optional 对象;否则返回一个空的 Optional 对象
flatMap如果值存在,就对该值执行提供的 mapping函数调用,返回一个 Optional 类型的值,否则就返 回一个空的 Optional 对象
get如果该值存在,将该值用 Optional 封装返回,否则抛出一个 NoSuchElementException 异常
ifPresent如果值存在,就执行使用该值的方法调用,否则什么也不做
isPresent如果值存在就返回 true,否则返回 false
map如果值存在,就对该值执行提供的 mapping函数调用
of将指定值用 Optional 封装之后返回,如果该值为 null,则抛出一个 NullPointerException 异常
ofNullable将指定值用 Optional 封装之后返回,如果该值为 null,则返回一个空的 Optional 对象
orElse如果有值则将其返回,否则返回一个默认值
orElseGet如果有值则将其返回,否则返回一个由指定的 Supplier 接口生成的值
orElseThrow如果有值则将其返回,否则抛出一个由指定的 Supplier 接口生成的异常
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值