使用Java 8 Optional避免空指针异常

Optional可让你的代码具备可读性,且会避免出现空指针异常。java

都说没有遇到过空指针异常的程序员不是Java程序员,null确实引起过不少问题。Java 8中引入了一个叫作java.util.Optional的新类能够避免null引发的诸多问题。程序员

咱们看一下null引用能致使哪些危害。首先建立一个类Computer,结构以下图所示:安全

输入图片说明

当咱们调用以下代码会怎样?微信

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

上述代码看似是没有问题的,可是不少计算机(好比,树莓派)实际上是没有声卡的,那么调用getSoundcard()方法可定会抛出空指针异常了。app

一个常规的可是很差的的方法是返回一个null引用来表示计算机没有声卡,可是这就意味着会对一个空引调用getUSB()方法,显然会在程序运行过程当中抛出控制异常,从而致使程序中止运行。想一想一下,当你的程序在客户端电脑上运行时,忽然出现这种错是多尴尬的一件事?  伟大计算机科学Tony Hoare曾经写到:"我认为null引用从1965年被创造出来致使了十亿美圆的损失。当初使用null引用对我最大的诱惑就是它实现起来方便。"函数

那么该怎么避免在程序运行时会出现空指针异常呢?你须要保持警戒,而且不断检查可能出现空指针的状况,就像下面这样:ui

String version = "UNKNOWN";
if(computer != null)
    {
        Soundcard soundcard = computer.getSoundcard();
        if(soundcard != null){
             USB usb = soundcard.getUSB();
             if(usb != null){
                 version = usb.getVersion();
                }
            }
    }

然而,你能够看到上述代码有太多的null检查,整个代码结构变得很是丑陋。可是咱们又不得不经过这样的判断来确保系统运行时不会出现空指针。若是在咱们的业务代码中出现大量的这种空引用判断简直让人恼火,也致使咱们代码的可读性会不好。设计

若是你忘记检查要给值是否为空,null引用也是存在很大的潜在问题。这篇文章我将证实使用null引用做为值不存在的表示是很差的方法。咱们须要一个更好的表示值不存在的模型,而不是再使用null引用。指针

Java 8引入了一个新类叫作java.util.Optional<T>,这个类的设计的灵感来源于Haskell语言和Scala语言。这个类能够包含了一个任意值,像下面图和代码表示的那样。你能够把Optional看作是一个有可能包含了值的值,若是Optional不包含值那么它就是空的,下图那样。code

输入图片说明

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(){ ... }
}

上述代码展示了一台计算机有可能包换一个声卡(声卡是有可能存在也有可能不存在)。声卡也是有可能包含一个USB端口的。这是一种改善方法,该模型能够更加清晰的反映一个被给定的值是能够不存在的。

可是该怎么处理Optional<Soundcard>这个对象呢?毕竟,你想要获取的是USB的端口号。很简单,Optional类包含了一些方法来处理值是否存在的情况。和null引用相比Optional类迫使你在你要作值是否相关处理,从而避免了空指针异常。

须要说明的是Optional类并非要取代null引用。相反地,是为了让设计的API更容易被理解,当你看到一个函数的签名时,你就能够判断要传递给这个函数的值是否是有可能不存在。这就促使你要打开Optional类来处理确实值的情况了。

采用Optional模式

啰嗦了这么多,来看一些代码吧!我们先看一下怎么使用Optional改写传统的null引用检测后是什么样子。在这边文章的末尾你将会明白怎么使用Optional。

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

建立Optional对象

能够建立一个空的Optional对象:

Optional<Soundcard> sc = Optional.empty();

接下来是建立一个包含非null值的Optional:

SoundCard soundcard = new Soundcard();
Optional<Soundcard> sc = Optional.of(soundcard);

若是声卡null,空指针异常会当即被抛出(这比在获取声卡属性时才抛出要好)。

经过使用ofNullable,你能够建立一个可能包含null引用的Optional对象:

Optional<Soundcard> sc = Optional.ofNullable(soundcard);

若是声卡是null 引用,Optional对象就是一个空的。

对Optional中的值的处理

既然如今已经有了Optional对象,你能够调用相应的方法来处理Optional对象中的值是否存在。和进行null检测相比,咱们可使用ifPresent()方法,像下面这样:

Optional<Soundcard> soundcard = ...;
soundcard.ifPresent(System.out::println);

这样就没必要再作null检测,若是Optional对象是空的,那么什么信息将不会打印出来。

你也可使用isPresent()方法查看Optional对象是否真的存在。另外,还有一个get()方法能够返回Optional对象中的包含的值,若是存在的话。不然会抛出一个NoSuchElementException异常。这两个方式能够像下面这样搭配起来使用,从而避免异常:

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

可是这种方式不推荐使用(它和null检测相比没有什么改进),下面咱们将会探讨一下工做惯用的方式。

返回默认值和相关操做

当遇到null时一个常规的操做就是返回一个默认值,你可使用三元表达式来实现:

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

使用Optional对象的话,你能够orElse()使用重写,当Optional是空的时候orElse()能够返回一个默认值:

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

相似地,当Optional为空的时候也可使用orElseThrow()抛出异常:

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方法须要一个predicate对向做为参数。若是Optional中的值存在而且知足predicate,那么filter函数将会返回知足条件的值;不然,会返回一个空的Optional对象。

使用map方法进行数据的提取和转化

一个常见的模式是提取一个对象的一些属性。好比,对于一个Soundcard对象,你可能须要获取它的USB对象,而后判断它的的版本号。一般咱们的实现方式是这样的:

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

咱们可使用map方法重写这种检测null,而后再提取对象类型的对象。

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

这个和使用stream的map函数式同样的。使用stream须要给map函数传递一个函数做为参数,这个传递进来的函数将会应用于stream中的每一个元素。当stream时空的时候,什么也不会发生。

Optional中包含的值将会被传递进来的函数转化(这里是一个从声卡中获取USB的函数)。若是Optional对象时空的,那么什么也不会发生。

而后,咱们结合map方法和filter方法过滤掉USB的版本号不是3.0的声卡。

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

这样咱们的代码开始变得像有点像开始咱们给出的样子,没有了null检测。

使用flatMap函数传递Optional对象

如今已经介绍了一个可使用Optional重构代码的例子,那么咱们应该如何使用安全的方式实现下面代码呢?

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

注意上面的代码都是从一个对象中提取另外一个对象,使用map函数能够实现。在前面的文章中咱们设置了Computer中包含的是一个Optional<Soundcard>对象,Soundcard包含的是一个Optional<USB>对象,所以咱们能够这么重构代码

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

不幸的是,上面的代码会编译错误,那么为何呢?computer变量是Optional<Computer>类型的,因此它调用map函数是没有问题的。可是getSoundcard()方法返回的是一个Optional<Soundcard>的对象,返回的是Optional<Optional<Soundcard>>类型的对象,进行了第二次map函数的调用,结果调用getUSB()函数就变成非法的了。下面的图描述了这种场景:

输入图片说明

map函数的源码实现是这样的:

public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
        Objects.requireNonNull(mapper);
        if (!isPresent())
            return empty();
        else {
            return Optional.ofNullable(mapper.apply(value));
        }
    }

能够看出map函数还会再调用一次Optional.ofNullable(),从而致使返回Optional<Optional<Soundcard>>

Optional提供了flatMap这个函数,它的设计意图是当对Optional对象的值进行转化(就像map操做)而后一个两级Optional压缩成一个。下面的图展现了Optional对象经过调用map和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对象。

咱们终于把刚开始使用的嵌套null检查的丑陋代码改写了可读性高的代码,也避免了空指针异常的出现的代码。

总结

在这片文章中咱们采用了Java 8提供的新类java.util.Optional<T>。这个类的初衷不是要取代null引用,而是帮助设计者设计出更好的API,只要读到函数的签名就可知道该函数是否接受一个可能存在也可能不存在的值。另外,Optional迫使你去打开Optional,而后处理值是否存在,这就使得你的代码避免了潜在的空指针异常。

最后

感谢阅读,有兴趣能够关注微信公众帐号获取最新推送文章。

欢迎关注微信公众帐号

相关文章
相关标签/搜索