利用Java8 Optional如何避免空指针异常详解

前言

空指针是我们最常见也最讨厌的异常,为了防止空指针异常,你不得在代码里写大量的非空判断。

Java 8引入了一个新的Optional类。用于避免空指针的出现,也无需在写大量的if(obj!=null)这样的判断了,前提是你得将数据用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,然后处理值是否存在,这就使得你的代码避免了潜在的空指针异常。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

您可能感兴趣的文章:

  • 使用Java8中Optional机制的正确姿势
  • Java8中Optional类型和Kotlin中可空类型的使用对比
  • java8中forkjoin和optional框架使用
  • Java中避免空指针异常的方法
  • java 代码中预防空指针异常的处理办法
  • 浅谈java异常处理之空指针异常
  • 由@NotNull注解引出的关于Java空指针的控制
  • Java函数式开发 Optional空指针处理
  • java 避免出现NullPointerException(空指针)的方法总结
(0)

相关推荐

  • 浅谈java异常处理之空指针异常

    听老师说,在以后的学习中大部分的异常都是空指针异常.所以抽点打游戏的时间来查询一下什么是空指针异常 一:空指针异常产生的主要原因如下: (1)当一个对象不存在时又调用其方法会产生异常obj.method() // obj对象不存在 (2)当访问或修改一个对象不存在的字段时会产生异常obj.method() // method方法不存在 (3)字符串变量未初始化: (4)接口类型的对象没有用具体的类初始化,比如: List lt:会报错 List lt = new ArrayList():则不会报

  • Java中避免空指针异常的方法

    没人会喜欢空指针异常!有什么方法可以避免它们吗?或许吧.. 本文将讨论到以下几种技术 1.Optional类型(Java 8中新引入的) 2.Objects类(Java 7中原有的) Java 8中的Optional类 它是什么? 1.Java 8中新引入的类型 2.它是作为某个指定类型的对象的包装器或者用于那些不存在对象(null)的场景 简单来说,它是处理空值的一个更好的替代品(警告:乍一看可能并没有那么明显) 基本用法 它是一种类型(一个类)--那么,怎么才能创建一个这个类型的实例? 使用

  • 使用Java8中Optional机制的正确姿势

    前言 Java8带来的函数式编程特性对于习惯命令式编程的程序员来说还是有一定的障碍的,我们只有深入了解这些机制的方方面面才能运用自如.Null的处理在JAVA编程中是出了try catch之外的另一个头疼的问题,需要大量的非空判断模板代码,程序逻辑嵌套层次太深.尤其是对集合的使用,需要层层判空. 首先来看下Optional类的结构图: 而如果我们对它不稍假探索, 只是轻描淡写的认为它可以优雅的解决 NullPointException 的问题, 于是代码就开始这么写了 Optional<User

  • Java函数式开发 Optional空指针处理

    摘要 空闲时会抽空学习同在jvm上运行的Groovy和Scala,发现他们对null的处理比早期版本Java慎重很多.在Java8中,Optional为函数式编程的null处理给出了非常优雅的解决方案.本文将说明长久以来Java中对null的蹩脚处理,然后介绍使用Optional来实现Java函数式编程. 那些年困扰着我们的null 在Java江湖流传着这样一个传说:直到真正了解了空指针异常,才能算一名合格的Java开发人员.在我们逼格闪闪的java码字符生涯中,每天都会遇到各种null的处理,

  • java 代码中预防空指针异常的处理办法

    项目中遇到的NullPointerException 分两种情况: 1. 引用空对象,即调用空对象的方法或引用空对象的属性. 2. 将8中基础类型的封装类赋值给对应的基础类. 1.对于别人接口的返回对象要做非空判断,因为我们不清楚获得的对象会不会为空,对于Collection Map 我一般会调用CollectionUtils MapUtils ,对于返回的String对象,我会调StringUtils.isNotEmpty()进行非空判断.他们中isNotEmpty不仅判断了NULL 还判断了

  • java 避免出现NullPointerException(空指针)的方法总结

    java 避免出现NullPointerException(空指针)的方法总结 Java应用中抛出的空指针异常是解决空指针的最好方式,也是写出能顺利工作的健壮程序的关键.俗话说"预防胜于治疗",对于这么令人讨厌的空指针异常,这句话也是成立的.值得庆幸的是运用一些防御性的编码技巧,跟踪应用中多个部分之间的联系,你可以将Java中的空指针异常控制在一个很好的水平上.顺便说一句,这是Javarevisited上的第二个空指针异常的帖子.在上个帖子中我们讨论了Java中导致空指针异常的常见原因

  • 由@NotNull注解引出的关于Java空指针的控制

    Java 小技巧和在java应用避免NullPonintException的最佳方法 在java应用程序中,一个NullPonintException(空指针异常)是最好解决(问题)的方法.同时,空指针也是写健壮的顺畅运行的代码的关键."预防好过治疗"这句话也同样适用于令人不爽的NullPonintException.通过应用防御性的编码技术和在遵守多个部分之间的约定,你可以再很大程度上避免NullPointException.下面的这些java小技巧可以最小化像!=null这种检查的

  • java8中forkjoin和optional框架使用

    并行流与串行流 并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流. java 8 中将并行进行了优化,我们可以很容易的对数据进行并行操作.Stream API 可以声明性地通过 parallel()与 sequential()在并行流与顺序流之间进行切换. 了解 Fork/Join 框架 Fork/Join 框架:就是在必要的情况下,将一个大任务,进形拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运行的结果进行join汇总. Fork/Join 框架

  • Java8中Optional类型和Kotlin中可空类型的使用对比

    本文主要给大家介绍了关于Java8中Optional类型和Kotlin中可空类型使用的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍: 在 Java 8中,我们可以使用 Optional 类型来表达可空的类型. package com.easy.kotlin; import java.util.Optional; import static java.lang.System.out; /** * Optional.ofNullable - 允许传递为 null 参数 *

  • 利用Java8 Optional如何避免空指针异常详解

    前言 空指针是我们最常见也最讨厌的异常,为了防止空指针异常,你不得在代码里写大量的非空判断. Java 8引入了一个新的Optional类.用于避免空指针的出现,也无需在写大量的if(obj!=null)这样的判断了,前提是你得将数据用Optional装着,它就是一个包裹着对象的容器. 都说没有遇到过空指针异常的程序员不是Java程序员,null确实引发过很多问题.Java 8中引入了一个叫做java.util.Optional的新类可以避免null引起的诸多问题. 我们看看一个null引用能导

  • Java8常用的新特性详解

    一.Java 8 新特性的简介 速度更快 代码更少(增加了新的语法:Lambda表达式)强大的Stream API 便于并行 最大化减少空指针异常:Optional Nashorn引擎,允许在JVM上运行JS应用 二.Lambda表达式 Lambda表达式:特殊的匿名内部类,语法更简洁. Lanbda表达式允许把函数作为一个方法的参数(函数作为方法参数传递),将代码像数据一样传递. 基本语法: <函数式接口> <变量名> = (参数1,参数2...) ->{ //方法体 }

  • 利用Java8 Optional类优雅如何地解决空指针问题

    前言 Java8 由Oracle在2014年发布,是继Java5之后最具革命性的版本. Java8吸收其他语言的精髓带来了函数式编程,lambda表达式,Stream流等一系列新特性,学会了这些新特性,可以让你实现高效编码优雅编码. 1. 不受待见的空指针异常 有个小故事:null引用最早是由英国科学家Tony Hoare提出的,多年后Hoare为自己的这个想法感到后悔莫及,并认为这是"价值百万的重大失误".可见空指针是多么不受待见. NullPointerException是Java

  • Java8新特性 StreamAPI实例详解

    目录 Stream结果收集 结果收集到集合中 结果收集到数组中 对流中的数据做聚合计算 对流中数据做分组操作 对流中的数据做分区操作 对流中的数据做拼接 并行的Stream流 串行的Stream流 并行流 获取并行流 并行流操作 并行流和串行流对比 线程安全问题 Stream结果收集 面试官:说说你常用的StreamAPI. 结果收集到集合中 public static void main(String[] args){ // Stream<String> stream = Stream.of

  • 微信小程序 利用css实现遮罩效果实例详解

    微信小程序 利用css实现遮罩效果实例详解 实现效果图: 如图所示,使用css实现小程序的遮罩效果,代码如下 js文件代码: //index.js //获取应用实例 var app = getApp() Page({ data: { flag: false }, a: function(){ this.setData({flag: false}) }, b: function(){ this.setData({flag: true}) } }) wxss文件代码: .b1{position:fi

  • 利用二进制文件安装etcd的教程详解

    etcd组件作为一个高可用强一致性的服务发现存储仓库. etcd作为一个受到ZooKeeper与doozer启发而催生的项目,除了拥有与之类似的功能外,更专注于以下四点. 简单:基于HTTP+JSON的API让你用curl就可以轻松使用. 安全:可选SSL客户认证机制. 快速:每个实例每秒支持一千次写操作. 可信:使用Raft算法充分实现了分布式. 场景一:服务发现(Service Discovery)一个强一致性.高可用的服务存储目录.基于Raft算法的etcd天生就是这样一个强一致性高可用的

  • python 利用pyttsx3文字转语音过程详解

    这篇文章主要介绍了python 利用pyttsx3文字转语音过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 # -*- coding: utf-8 -*- import pyttsx3 engine = pyttsx3.init() with open("all.txt",'r',encoding='utf-8') as f: while 1: line = f.readline() print(line, end = '')

  • 对DataFrame数据中的重复行,利用groupby累加合并的方法详解

    pandas读取一组数据,可能存在重复索引,虽然可以利用drop_duplicate直接删除,但是会删除重要信息. 比如同一ID用户,多次登录学习时间.要计算该用户总共''学习时间'',就要把重复的ID的''学习时间''累加. 可以结合groupby和sum函数完成该操作. 实例如下: 新建一个DataFrame,计算每个 id 的总共学习时间.其中 id 为one/two的存在重复学习时间.先利用 groupby 按照键 id 分组,然后利用sum()函数求和,即可得到每个id的总共学习时间.

  • Java8 Comparator排序方法实例详解

    这篇文章主要介绍了Java8 Comparator排序方法实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 Java8 中 Comparator 接口提供了一些静态方法,可以方便于我们进行排序操作,下面通过例子讲解下如何使用 对整数列表排序(升序) List<Integer> list = Arrays.asList(1, 4, 2, 6, 2, 8); list.sort(Comparator.naturalOrder()); Sys

  • 利用Python计算KS的实例详解

    在金融领域中,我们的y值和预测得到的违约概率刚好是两个分布未知的两个分布.好的信用风控模型一般从准确性.稳定性和可解释性来评估模型. 一般来说.好人样本的分布同坏人样本的分布应该是有很大不同的,KS正好是有效性指标中的区分能力指标:KS用于模型风险区分能力进行评估,KS指标衡量的是好坏样本累计分布之间的差值. 好坏样本累计差异越大,KS指标越大,那么模型的风险区分能力越强. 1.crosstab实现,计算ks的核心就是好坏人的累积概率分布,我们采用pandas.crosstab函数来计算累积概率

随机推荐