看山聊Java:一文掌握 Java8 的 Optional 的 6 种操作

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

image.png

你好,我是看山。


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实例,可以借助of和ofNullable两个方法实现。


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


代码如下:


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

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


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

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


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


获取数据:get

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

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 != null,ifPresent可以传入一个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

map和flatMap是对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所在班级名称,在定义的时候,我们将s2的clazz属性定义为 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操作结合,一起完成对象操作。当值为空时,orElse和orElseGet返回默认值,orElseThrow抛出指定的异常。


orElse和orElseGet的区别是,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。


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


Optional是一个包装类,且不可变,不可序列化

没有公共构造函数,创建需要使用of、ofNullable方法

空Optional是单例,都是引用Optional.EMPTY

想要获取Optional的值,可以使用get、orElse、orElseGet、orElseThrow

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


使用get方法前,必须使用isPresent检查。但是使用isPresent前,先思考下是否可以使用orElse、orElseGet等方法代替实现。

orElse和orElseGet,优先选择orElseGet,这个是惰性计算

Optional不要作为参数或者类属性,可以作为返回值

尽量将map、filter的函数参数抽出去作为单独方法,这样能够保持链式调用

推荐阅读

一文掌握 Java8 Stream 中 Collectors 的 24 个操作

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

目录
相关文章
|
5月前
|
安全 Java API
告别繁琐编码,拥抱Java 8新特性:Stream API与Optional类助你高效编程,成就卓越开发者!
【8月更文挑战第29天】Java 8为开发者引入了多项新特性,其中Stream API和Optional类尤其值得关注。Stream API对集合操作进行了高级抽象,支持声明式的数据处理,避免了显式循环代码的编写;而Optional类则作为非空值的容器,有效减少了空指针异常的风险。通过几个实战示例,我们展示了如何利用Stream API进行过滤与转换操作,以及如何借助Optional类安全地处理可能为null的数据,从而使代码更加简洁和健壮。
142 0
|
6月前
|
Java BI 数据处理
如何在Java中实现Excel操作
如何在Java中实现Excel操作
|
4月前
|
设计模式 Java
结合HashMap与Java 8的Function和Optional消除ifelse判断
`shigen`是一位致力于记录成长、分享认知和留住感动的博客作者。本文通过具体代码示例探讨了如何优化业务代码中的if-else结构。首先展示了一个典型的if-else处理方法,并指出其弊端;然后引入了策略模式和工厂方法等优化方案,最终利用Java 8的Function和Optional特性简化代码。此外,还提到了其他几种消除if-else的方法,如switch-case、枚举行、SpringBoot的IOC等。一起跟随shigen的脚步,让每一天都有所不同!
45 10
结合HashMap与Java 8的Function和Optional消除ifelse判断
|
3月前
|
Java 编译器 API
从Java 8到Java 17,这些新特性让你的代码起飞!
【10月更文挑战第10天】在软件开发领域,Java作为一种历史悠久且广泛使用的编程语言,不断进化以适应新的需求和挑战。从Java 8到Java 17,每一次版本更新都带来了诸多新特性和改进,极大地提升了开发效率和代码质量。今天,我们就来一起探讨这些新特性,看看它们是如何让我们的代码“起飞”的。
232 0
|
5月前
|
存储 算法 Oracle
19 Java8概述(Java8概述+lambda表达式+函数式接口+方法引用+Stream+新时间API)
19 Java8概述(Java8概述+lambda表达式+函数式接口+方法引用+Stream+新时间API)
76 8
|
6月前
|
存储 Java 索引
Java ArrayList操作指南:如何移除并返回第一个元素
通过上述方法,你可以方便地从Java的 `ArrayList` 中移除并返回第一个元素。这种操作在日常编程中非常常见,是处理列表时的基本技能之一。希望这篇指南能帮助你更好地理解和运用Java的 `ArrayList`。
69 4
|
5月前
|
安全 Java API
Java 8 流库的魔法革命:Filter、Map、FlatMap 和 Optional 如何颠覆编程世界!
【8月更文挑战第29天】Java 8 的 Stream API 通过 Filter、Map、FlatMap 和 Optional 等操作,提供了高效、简洁的数据集合处理方式。Filter 用于筛选符合条件的元素;Map 对元素进行转换;FlatMap 将多个流扁平化合并;Optional 安全处理空值。这些操作结合使用,能够显著提升代码的可读性和简洁性,使数据处理更为高效和便捷。
183 0
|
5月前
|
Java API
Java8 Lambda 设计和实现问题之在Java 8的Stream API中,parallel=false时collect方法是如何实现的
Java8 Lambda 设计和实现问题之在Java 8的Stream API中,parallel=false时collect方法是如何实现的
|
6月前
|
分布式计算 DataWorks Java
DataWorks操作报错合集之使用ODPS Tunnel Upload功能时,遇到报错:Java 堆内存不足,该如何解决
DataWorks是阿里云提供的一站式大数据开发与治理平台,支持数据集成、数据开发、数据服务、数据质量管理、数据安全管理等全流程数据处理。在使用DataWorks过程中,可能会遇到各种操作报错。以下是一些常见的报错情况及其可能的原因和解决方法。
|
6月前
|
SQL 缓存 Java
使用MyBatis优化Java持久层操作
使用MyBatis优化Java持久层操作
下一篇
开通oss服务