一文掌握 Java8 的 Optional 的 6 种操作

一文掌握 Java8 的 Optional 的 6 种操作

你好,我是看山。

Java8 中引入了一个特别有意思类:Optional,一个可以让我们更加轻松的避免 NPE(空指针异常,NullPointException)的工具。

很久很久以前,为了避免 NPE,我们会写很多类似if (obj != null) {}的代码,有时候忘记写,就可能出现 NPE,造成线上故障。在 Java 技术栈中,如果谁的代码出现了 NPE,有极大的可能会被笑话,这个异常被很多人认为是低级错误。Optional的出现,可以让大家更加轻松的避免因为低级错误被嘲讽的概率。

定义示例数据

先定义待操作对象,万能的Student类和Clazz类(用到了 lombok 和 guava):

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Clazz {
    private String id;
    private String name;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
    private String id;
    private String name;
    private Clazz clazz;
}

然后定义一组测试数据:

final Clazz clazz1 = new Clazz("1", "高一一班");

final Student s1 = new Student("1", "张三", clazz1);
final Student s2 = new Student("2", "李四", null);

final List<Student> students = Lists.newArrayList(s1, s2);
final List<Student> emptyStudents = Lists.newArrayList();
final List<Student> nullStudents = null;

创建实例:of、ofNullable

为了控制生成实例的方式,也是为了收紧空值Optional的定义,Optional将构造函数定义为private。想要创建Optional实例,可以借助ofofNullable两个方法实现。

这两个方法的区别在于:of方法传入的参数不能是null的,否则会抛出NullPointerException。所以,对于可能是null的结果,一定使用ofNullable

代码如下:

Optional.of(students);
Optional.of(emptyStudents);
Optional.ofNullable(nullStudents);

Optional类中还有一个静态方法:empty,这个方法直接返回了内部定义的一个常量Optional<?> EMPTY = new Optional<>(),这个常量的valuenullofNullable方法也是借助了empty实现null的包装:

public static <T> Optional<T> ofNullable(T value) {
    return value == null ? empty() : of(value);
}

所以说,对于nullOptional包装类,指向的都是相同的实例对象,Optional.empty() == Optional.ofNullable(null)返回的是true。换句话说,空Optional是单例的。

为了方便描述,下文中对值为nullOptional统称为“Optional”。

获取数据:get

Optionalget方法有些坑人,先看下它的源码:

public T get() {
    if (value == null) {
        throw new NoSuchElementException("No value present");
    }
    return value;
}

也就是说,Optional值为空时,使用get方法将抛出NoSuchElementException异常。如果不想抛出异常,或者能够 100%确定不是空Optional,或者使用isPresent方法判断。

如果能 100%确定不是空Optional,那就没有必要使用Optional包装,直接返回即可。如果需要使用isPresent方法,那就和直接判空没有区别了。所以,无论是第一种情况还是第二种情况,都违背了设计这个类的初衷。

值为空判断:isPresent、ifPresent

isPresent用来判断值是否为空,类似于obj != nullifPresent可以传入一个Consumer操作,当值不为空的时候,会执行Consumer函数。比如:

final Optional<List<Student>> nullValue = Optional.ofNullable(nullStudents);

if (nullValue.isPresent()) {
    System.out.println("value: " + nullValue.get());
}

上面的方法等价于:

nullValue.ifPresent(value -> System.out.println("value: " + value));

isPresent判断的写法上是不是感觉很熟悉,感觉可以直接写为:

if (nullStudents != null) {
    System.out.println("value: " + nullStudents);
}

对于isPresent,如果是在自己可控的代码范围内,完全没有必要将值封装之后再判空。对于自己不可控的代码,后续的filter或者map方法可能比isPresent更好用一些。

对于ifPresent,在使用的时候会有一些限制,就是必须是非空Optional的时候,在会执行传入的Consumer函数。

值处理:map、flatMap

mapflatMap是对Optional的值进行操作的方法,区别在于,map会将结果包装到Optional中返回,flatMap不会。但是两个方法返回值都是Optional类型,这也就要求,flatMap的方法函数返回值需要是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));
    }
}

可以看到,如果Optional的值为空,map直接返回Optional.EMPTY,否则会执行函数结果,并使用Optional.ofNullable包装并返回。也即是说,只要类结构允许,我们可以一直map下去,就像是扒洋葱,一层一层,直到核心。

比如,我们要获取s2所在班级名称,在定义的时候,我们将s2clazz属性定义为 null,如果以前需要写为:

String clazzNameOld;
if (s2 != null && s2.getClazz() != null && s2.getClazz().getName() != null) {
    clazzNameOld = s2.getClazz().getName();
} else {
    clazzNameOld = "DEFAULT_NAME";
}

现在借助Optional可以写为:

final String clazzName = Optional.ofNullable(s2)
        .map(Student::getClazz)
        .map(Clazz::getName)
        .orElse("DEFAULT_NAME");

从代码上似乎没有多大改变,但是如果Clazz内部还有类对象。或者,我们在if判断的时候,少写一层检查呢?而且,map的精巧还在于它的返回值永远是Optional,这样,我们可以重复调用map方法,而不需要中间被打断,增加各种判空逻辑。

值为空的处理:orElse、orElseGet、orElseThrow

这几个方法可以与map操作结合,一起完成对象操作。当值为空时,orElseorElseGet返回默认值,orElseThrow抛出指定的异常。

orElseorElseGet的区别是,orElse方法传入的参数是明确的默认值,orElseGet方法传入的参数是获取默认值的函数。如果默认值的构造过程比较复杂,需要经过一系列的运算逻辑,那一定要使用orElseGet,因为orElseGet是在值为空的时候,才会执行函数,并返回默认值,如果值不为空,则不会执行函数,相比于orElse而言,减少了一次构造默认值的过程。

同样以上面的例子:

orElse的写法:

final String clazzName = Optional.ofNullable(s2)
        .map(Student::getClazz)
        .map(Clazz::getName)
        .orElse(null);

orElseGet的写法:

final String clazzName = Optional.of(s2)
        .map(Student::getClazz)
        .map(Clazz::getName)
        .orElseGet(() -> null);

如果clazz属性一定不为空,为空则返回异常,可以使用orElseThrow

final String clazzName = Optional.of(s2)
        .map(Student::getClazz)
        .map(Clazz::getName)
        .orElseThrow(() -> new IllegalArgumentException("clazz属性不合法"));

条件过滤:filter

filter方法提供的是值验证,如果值验证为 true,返回当前值;否则,返回空Optional。比如,我们要遍历students,找到班级属性为空的,打印学生id:

for (final Student s : students) {
    Optional.of(s)
            .filter(x -> x.getClazz() == null)
            .ifPresent(x -> System.out.println(x.getId()));
}

其他:equals、hashCode、toString

Optional重写了这三个方法。因为Optional可以认为是包装类,所以还是围绕这被包装的值重写这三个方法。下面给出这三个方法的源码:

public boolean equals(Object obj) {
    // 同一对象判断
    if (this == obj) {
        return true;
    }

    // 类型判断
    if (!(obj instanceof Optional)) {
        return false;
    }

    Optional<?> other = (Optional<?>) obj;
    // 最终还是值的判断
    return Objects.equals(value, other.value);
}

public int hashCode() {
    // 直接返回值的hashCode
    return Objects.hashCode(value);
}

public String toString() {
    return value != null
        ? String.format("Optional[%s]", value) // 用到了值的toString结果
        : "Optional.empty";
}

equals方法,Optional.of(s1).equals(Optional.of(s2))完全等价于s1.equals(s2)

hashCode方法,直接返回的是值的hashCode,如果是空Optional,返回的是0。

toString方法,为了能够识别是Optional,将打印数据包装了一下。如果是空Optional,返回的是字符串“Optional.empty”;如果是非空,返回是是“Optional[值的toString]”。

文末总结

NPE 之所以讨厌,就是只要出现 NPE,我们就能够解决。但是一旦出现,都已经是事后,可能已经出现线上故障。偏偏在 Java 语言中,NPE 又很容易出现。Optional提供了模板方法,有效且高效的避免 NPE。

接下来,我们针对上面的使用,总结一下:

  1. Optional是一个包装类,且不可变,不可序列化
  2. 没有公共构造函数,创建需要使用ofofNullable方法
  3. Optional是单例,都是引用Optional.EMPTY
  4. 想要获取Optional的值,可以使用getorElseorElseGetorElseThrow

另外,还有一些实践上的建议:

  1. 使用get方法前,必须使用isPresent检查。但是使用isPresent前,先思考下是否可以使用orElseorElseGet等方法代替实现。
  2. orElseorElseGet,优先选择orElseGet,这个是惰性计算
  3. Optional不要作为参数或者类属性,可以作为返回值
  4. 尽量将mapfilter的函数参数抽出去作为单独方法,这样能够保持链式调用

推荐阅读

  • 一文掌握 Java8 Stream 中 Collectors 的 24 个操作
  • 一文掌握 Java8 的 Optional 的 6 种操作

你好,我是看山,公众号:看山的小屋,10 年老猿,开源贡献者。游于码界,戏享人生。

个人主页:https://www.howardliu.cn
个人博文:一文掌握 Java8 的 Optional 的 6 种操作
CSDN 主页:http://blog.csdn.net/liuxinghao
CSDN 博文:一文掌握 Java8 的 Optional 的 6 种操作

公众号:看山的小屋

热门文章

暂无图片
编程学习 ·

C语言二分查找详解

二分查找是一种知名度很高的查找算法&#xff0c;在对有序数列进行查找时效率远高于传统的顺序查找。 下面这张动图对比了二者的效率差距。 二分查找的基本思想就是通过把目标数和当前数列的中间数进行比较&#xff0c;从而确定目标数是在中间数的左边还是右边&#xff0c;将查…
暂无图片
编程学习 ·

GMX 命令分类列表

建模和计算操作命令&#xff1a; 1.1 . 创建拓扑与坐标文件 gmx editconf - 编辑模拟盒子以及写入子组(subgroups) gmx protonate - 结构质子化 gmx x2top - 根据坐标生成原始拓扑文件 gmx solvate - 体系溶剂化 gmx insert-molecules - 将分子插入已有空位 gmx genconf - 增加…
暂无图片
编程学习 ·

一文高效回顾研究生课程《数值分析》重点

数值分析这门课的本质就是用离散的已知点去估计整体&#xff0c;就是由黑盒子产生的结果去估计这个黑盒子。在数学里这个黑盒子就是一个函数嘛&#xff0c;这门课会介绍许多方法去利用离散点最大化地逼近这个函数&#xff0c;甚至它的导数、积分&#xff0c;甚至微分方程的解。…
暂无图片
编程学习 ·

在职阿里5年,一个28岁女软测工程师的心声

简单的先说一下&#xff0c;坐标杭州&#xff0c;14届本科毕业&#xff0c;算上年前在阿里巴巴的面试&#xff0c;一共有面试了有6家公司&#xff08;因为不想请假&#xff0c;因此只是每个晚上去其他公司面试&#xff0c;所以面试的公司比较少&#xff09; ​ 编辑切换为居中…
暂无图片
编程学习 ·

字符串左旋c语言

目录 题目&#xff1a; 解题思路&#xff1a; 第一步&#xff1a; 第二步&#xff1a; 第三步&#xff1a; 总代码&#xff1a; 题目&#xff1a; 实现一个函数&#xff0c;可以左旋字符串中的k个字符。 例如&#xff1a; ABCD左旋一个字符得到BCDA ABCD左旋两个字符…
暂无图片
编程学习 ·

设计模式--观察者模式笔记

模式的定义与特点 观察者&#xff08;Observer&#xff09;模式的定义&#xff1a;指多个对象间存在一对多的依赖关系&#xff0c;当一个对象的状态发生改变时&#xff0c;所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式&#xf…
暂无图片
编程学习 ·

睡觉突然身体动不了,什么是睡眠痽痪症

很多朋友可能有这样的体验&#xff0c;睡觉过程中突然意识清醒&#xff0c;身体却动弹不了。这时候感觉非常恐怖&#xff0c;希望旁边有一个人推自己一下。阳光以前也经常会碰到这样的情况&#xff0c;一年有一百多次&#xff0c;那时候很害怕晚上到来&#xff0c;睡觉了就会出…
暂无图片
编程学习 ·

深入理解C++智能指针——浅析MSVC源码

文章目录unique_ptrshared_ptr 与 weak_ptrstd::bad_weak_ptr 异常std::enable_shared_from_thisunique_ptr unique_ptr 是一个只移型别&#xff08;move-only type&#xff0c;只移型别还有std::mutex等&#xff09;。 结合一下工厂模式&#xff0c;看看其基本用法&#xff…
暂无图片
编程学习 ·

@TableField(exist = false)

TableField(exist false) //申明此字段不在数据库存在&#xff0c;但代码中需要用到它&#xff0c;通知Mybatis-plus在做写库操作是忽略它。,.
暂无图片
编程学习 ·

Java Web day15

第十二章文件上传和下载 一、如何实现文件上传 要实现Web开发中的文件上传功能&#xff0c;通常需要完成两步操作&#xff1a;一.是在Web页面中添加上传输入项&#xff1b;二是在Servlet中读取上传文件的数据&#xff0c;并保存到本地硬盘中。 需要使用一个Apache组织提供一个…
暂无图片
编程学习 ·

【51nod 2478】【单调栈】【前缀和】小b接水

小b接水题目解题思路Code51nod 2478 小b接水 题目 输入样例 12 0 1 0 2 1 0 1 3 2 1 2 1输出样例 6解题思路 可以发现最后能拦住水的都是向两边递减高度&#xff08;&#xff1f;&#xff09; 不管两个高积木之间的的积木是怎样乱七八糟的高度&#xff0c;最后能用来装水的…
暂无图片
编程学习 ·

花了大半天写了一个UVC扩展单元调试工具

基于DIRECTSHOW 实现的&#xff0c;用的是MFC VS2019. 详见&#xff1a;http://www.usbzh.com/article/detail-761.html 获取方法 加QQ群:952873936&#xff0c;然后在群文件\USB调试工具&测试软件\UVCXU-V1.0(UVC扩展单元调试工具-USB中文网官方版).exe USB中文网 USB中文…
暂无图片
编程学习 ·

贪心(一):区间问题、Huffman树

区间问题 例题一&#xff1a;区间选点 给定 N 个闭区间 [ai,bi]请你在数轴上选择尽量少的点&#xff0c;使得每个区间内至少包含一个选出的点。 输出选择的点的最小数量。 位于区间端点上的点也算作区间内。 输入格式 第一行包含整数 N&#xff0c;表示区间数。 接下来 …
暂无图片
编程学习 ·

C语言练习实例——费氏数列

目录 题目 解法 输出结果 题目 Fibonacci为1200年代的欧洲数学家&#xff0c;在他的着作中曾经提到&#xff1a;「若有一只免子每个月生一只小免子&#xff0c;一个月后小免子也开始生产。起初只有一只免子&#xff0c;一个月后就有两只免子&#xff0c;二个月后有三只免子…
暂无图片
编程学习 ·

Android开发(2): Android 资源

个人笔记整理 Android 资源 Android中的资源&#xff0c;一般分为两类&#xff1a; 系统内置资源&#xff1a;Android SDK中所提供的已经定义好的资源&#xff0c;用户可以直接拿来使用。 用户自定义资源&#xff1a;用户自己定义或引入的&#xff0c;只适用于当前应用的资源…
暂无图片
编程学习 ·

零基础如何在短时间内拿到算法offer

​算法工程师是利用算法处理事物的职业 算法&#xff08;Algorithm&#xff09;是一系列解决问题的清晰指令&#xff0c;也就是说&#xff0c;能够对一定规范的输入&#xff0c;在有限时间内获得所要求的输出。 如果一个算法有缺陷&#xff0c;或不适合于某个问题&#xff0c;执…
暂无图片
编程学习 ·

人工智能:知识图谱实战总结

人工智能python&#xff0c;NLP&#xff0c;知识图谱&#xff0c;机器学习&#xff0c;深度学习人工智能&#xff1a;知识图谱实战前言一、实体建模工具Protegepython&#xff0c;NLP&#xff0c;知识图谱&#xff0c;机器学习&#xff0c;深度学习 人工智能&#xff1a;知识图…
暂无图片
编程学习 ·

【无标题】

这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注…