阅读 828

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

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

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

我们看看一个null引用能导致哪些危害。首先创建一个类Computer,结构如下图所示:

输入图片说明
Computer类模型

当我们调用如下代码会怎样?

String version = computer.getSoundcard().getUSB().getVersion();
复制代码

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

一个常规的但是不好的的方法是返回一个null引用来表示计算机没有声卡,但是这就意味着会对一个空引调用getUSB()方法,显然会在程序运行过程中抛出控制异常,从而导致程序停止运行。想想一下,当你的程序在客户端电脑上运行时,突然出现这种错是多尴尬的一件事?

伟大计算机科学Tony Hoare曾经写到:"我认为null引用从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();
                }
            }
    }
复制代码

然而,你可以看到上述代码有太多的null检查,整个代码结构变得非常丑陋。但是我们又不得不通过这样的判断来确保系统运行时不会出现空指针。如果在我们的业务代码中出现大量的这种空引用判断简直让人恼火,也导致我们代码的可读性会很差。

如果你忘记检查要给值是否为空,null引用也是存在很大的潜在问题。这篇文章我将证明使用null引用作为值不存在的表示是不好的方法。我们需要一个更好的表示值不存在的模型,而不是再使用null引用。

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

输入图片说明
Optional模型

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包含的是一个Optional对象,因此我们可以这么重构代码

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

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

输入图片说明
Optional>

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进行类型转化的不同:

输入图片说明
map和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对象。

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

总结

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

最后

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

欢迎关注微信公众账号
欢迎关注微信公众账号

关注下面的标签,发现更多相似文章
评论