1

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

 2 years ago
source link: https://www.techug.com/post/master-6-operations-of-java-8-s-optional.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.


img16215662211639179181.jpeg

你好,我是看山。

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的函数参数抽出去作为单独方法,这样能够保持链式调

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK