文档章节

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

刘亚涛
 刘亚涛
发布于 01/11 14:39
字数 2692
阅读 4809
收藏 88

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

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

我们看一下null引用能导致哪些危害。首先创建一个类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不包含值那么它就是空的,下图那样。

输入图片说明

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,然后处理值是否存在,这就使得你的代码避免了潜在的空指针异常。

最后

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

欢迎关注微信公众账号

© 著作权归作者所有

共有 人打赏支持
刘亚涛
粉丝 20
博文 7
码字总数 17647
作品 0
武汉
程序员
加载中

评论(32)

刘亚涛
刘亚涛

引用来自“Kane.Sun”的评论

这东西guava里也有的吧 ?

回复@Kane.Sun : 是啊 java有参考其他语言
Kane.Sun
Kane.Sun
这东西guava里也有的吧 ?
南望山阿飞
南望山阿飞

引用来自“南望山阿飞”的评论

看了optional,我选择多谢几个if判断 ;)

引用来自“刘亚涛”的评论

作为一个南望山男子技校出来的怎么可以这么排斥新技术呢:smirk:
怕是校友没跑了:stuck_out_tongue_closed_eyes:
odilil
odilil
 怎么还有乱码?
izee
izee
我选择用IF
刘亚涛
刘亚涛

引用来自“豆浆王子”的评论

java8 lambda表达式这里面还有很多知识可以继续啃得!喜爱流式编程方法的朋友可以继续研究的

回复@豆浆王子 : java 8 和之前的版本相比是比较大的一次更新,很多东西懂理念上发生了变化,比如lambda表达式可以被当做形参传递给函数
刘亚涛
刘亚涛

引用来自“心尖偏左”的评论

和Scala相比还是太弱了

回复@心尖偏左 : 没有用过scala
刘亚涛
刘亚涛

引用来自“咚咚咚咚藕丁”的评论

可以啊TT

回复@咚咚咚咚藕丁 : 哎呦呦
心尖偏左
心尖偏左
和Scala相比还是太弱了
豆浆王子
豆浆王子
java8 lambda表达式这里面还有很多知识可以继续啃得!喜爱流式编程方法的朋友可以继续研究的
用Optional取代null

null引发的问题以及为什么要避免null引用 先看一个例子 意思也就是说获取一个人的车的保险公司的名称。可以看到的是,并不是每一个都有车,也并不是每一个车都有保险。这样的获取是会发生Nul...

小鱼嘻嘻
06/02
0
0
Java系列 – 用Java8新特性进行Java开发太爽了(续)

本人博客文章网址:https://www.peretang.com/using-java8s-new-features-to-coding-is-awesome-2/ 前言 上周, 我们谈论了关于Java8的新特性有那些, 什么是函数式编程, 什么是Lambda表达式, 这...

PereTang
2017/07/18
0
0
如何正确使用Java8的Optional机制

Java8带来的函数式编程特性对于习惯命令式编程的程序员来说还是有一定的障碍的,我们只有深入了解这些机制的方方面面才能运用自如。Null的处理在JAVA编程中是出了try catch之外的另一个头疼的...

欧阳海阳
06/21
0
0
Java函数式开发——优雅的Optional空指针处理

那些年困扰着我们的null 在Java江湖流传着这样一个传说:直到真正了解了空指针异常,才能算一名合格的Java开发人员。在我们逼格闪闪的java码字符生涯中,每天都会遇到各种null的处理,像下面...

随风溜达的向日葵
2016/08/28
8.4K
24
Java Optional空指针处理

那些年困扰着我们的null 在Java江湖流传着这样一个传说:直到真正了解了空指针异常,才能算一名合格的Java开发人员。在我们逼格闪闪的java码字符生涯中,每天都会遇到各种null的处理,像下面...

溜达向日葵
08/13
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Spring加载properties文件的两种方式

在项目中如果有些参数经常需要修改,或者后期可能需要修改,那我们最好把这些参数放到properties文件中,源代码中读取properties里面的配置,这样后期只需要改动properties文件即可,不需要修...

架构师springboot
10分钟前
0
0
分布式事务,原来可以这么玩?

多个数据要同时操作,如何保证数据的完整性,以及一致性? 答 : 事务 ,是常见的做法。 举个栗子: 用户下了一个订单,需要修改 余额表 , 订单 表 , 流水 表 ,于是会有类似的伪代码: st...

微笑向暖wx
12分钟前
0
0
IE6兼容PNG32图片显示PNG8图片

IE6并不是不支持PNG图片,只是不支持半透明通道。 是支持PNG8色表引索全透明的。 以往都是通过滤镜或统统使用PNG8实现兼容。 但是我发现twitter的png图标可以在chrome中显示png32,在IE6显示...

linsk1998
25分钟前
0
0
linux运维需要掌握的基础知识

踏入linux运维工程师这一职业,其实有很多工具技能需要掌握,下面我来给大家一一介绍。 1、shell脚本和另一个脚本语言,shell是运维人员必须具备的,不懂这个连入职都不行,至少也要写出一些...

linuxprobe16
26分钟前
0
0
《netty入门与实战》笔记-03:数据传输载体 ByteBuf 介绍

ByteBuf结构 首先,我们先来了解一下 ByteBuf 的结构 以上就是一个 ByteBuf 的结构图,从上面这幅图可以看到: ByteBuf 是一个字节容器,容器里面的的数据分为三个部分,第一个部分是已经丢弃...

Funcy1122
59分钟前
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部