优雅的剥洋葱:浅析 Java Optional 类

前言

  杨宗纬唱过一首歌,叫做《洋葱》,里面有一句是:“如果你愿意一层一层一层的剥开我的心。。。”,歌曲是非常的感人。

882ae86df6651eab552fc41a88f3c11.jpg

  其实在咱们程序员日常开发中,也经常会遇见需要一层一层剥洋葱的情况,如下图:

image.png

  比方说,现在咱们在后端需要请求某个接口,该接口返回格式如图,咱们需要得到items数组里第一项的value标签的值,可以发现,这就是个复杂对象一层一层 get 也就是一层一层剥洋葱的情况,每一次 get 都有可能为空,为了防止发生 NPE(空指针异常),咱们可能每一次 get 都得用 if 进行一个判空,像上图这种情况,可能得套四五个 if,代码非常臃肿。

  这也引出了本文的主题,Optional 类,此类可以用来解决上述问题。

Optional 类简析

类的结构和构造方法

  Optional 类的结构非常简单,如下:

public final class Optional<T> {
    /**
     * Common instance for {@code empty()}.
     */
    private static final Optional<?> EMPTY = new Optional<>(null);

    /**
     * If non-null, the value; if null, indicates no value is present
     */
    private final T value;

  此类有一个私有属性:value,这是用来表示实际对象的,Optional 类其实算是个包装类,将要处理的对象置于value,后面会说到;一个静态变量EMPTY,其实是个饿汉模式的单例,表示空对象对应的 Optional 对象。

  看完了它的属性,再看一下类的构造函数:

private Optional(T value) {
    this.value = value;
}

  类的构造方法非常简单,接收一个value 参数赋给自己的私有属性。注意到构造函数是 private 的,因此后面肯定有方法调用它来实例化对象的。

常用方法

of 方法

public static <T> Optional<T> of(T value) {
    return new Optional<>(Objects.requireNonNull(value));
}

  说曹操,曹操到。果然出现了调用构造方法的方法,还是赋值value属性,不过需要注意这里会调用Objects.requireNonNull(value),此方法还是会抛异常的,如下:

public static <T> T requireNonNull(T obj) {
    if (obj == null)
        throw new NullPointerException();
    return obj;
}

ofNullable 方法

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

  和 of 方法不同的是,此方法会先判断要转换的对象是否为 null,若是则返回(Optional<T>) EMPTY,否则才会调用构造方法。可见,这个方法还是比较省心的,不用考虑太多 null 的问题,也不用担心抛 NPE。

判空方法

public boolean isPresent() {
    return value != null;
}

public void ifPresent(Consumer<? super T> action) {
    if (value != null) {
        action.accept(value);
    }
}

  两个方法比较类似,就放在一块了,区别就是一个只是判断value是否为 null,另一个在判为非 null 之后会调用传入的 Consumer 接口对象对value进行后续处理。

empty 方法

public static<T> Optional<T> empty() {
    @SuppressWarnings("unchecked")
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
}

  返回一个value为空的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));
    }
}

public <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent()) {
        return empty();
    } else {
        @SuppressWarnings("unchecked")
        Optional<U> r = (Optional<U>) mapper.apply(value);
        return Objects.requireNonNull(r);
    }
} 

  map 方法有两个,都是先判断 value 是否为 null,若是,直接返回空的 Optional 类对象,否则,调用函数接口对象 mapper 的 apply 方法。

  map 和 flatMap 方法的主要不同在于调用的函数接口对应的 apply 方法的返回值不同,map 方法返回的就是一个普通的类型,确定value非空后,会调用Optional.ofNullable()自动将mapper.apply(value)的返回值包成一个 Optional 类的对象,比较方便省心;而 flatMap 方法需要你提供的函数接口的 apply 方法本身就返回 Optional 类的对象,而且也有抛出 NPE 的风险,比较费心。

  当然,也不能通过所谓的省心费心来判断哪个方法更好,有些情况下,选择 flatMap 是更合适的。

filter 方法

public Optional<T> filter(Predicate<? super T> predicate) {
    Objects.requireNonNull(predicate);
    // 检查 value 是否为 null
    if (!isPresent()) {
        return this;
    } else {
        return predicate.test(value) ? this : empty();
    }
}

  代码也比较简单,先看看value是否为 null,若是,直接返回this,不做处理;否则,调用predicate.test(value)看看value是否满足我们定义的某种规则,若不满足,返回empty();,方法如其名,是一个用来过滤的方法。

orElse 方法

// value 为 null,返回默认值
public T orElse(T other) {
    return value != null ? value : other;
}
// value 为 null,返回函数接口计算的值
public T orElseGet(Supplier<? extends T> supplier) {
    return value != null ? value : supplier.get();
}
// value 为 null,抛出异常
public T orElseThrow() {
    if (value == null) {
        throw new NoSuchElementException("No value present");
    }
    return value;
}

  三个方法都是拿到value值的,拿之前会做是否为 null 的判断,为 null 时,三个方法各自选择不同的处理办法,可以根据情景选择合适的方法。

小结

  看完了 Optional 类的结构和主要方法,可以先做个小结了:Optional 类其实算是个包装类,我们可以把想处理的对象包在 Optional 类里面,Optional 类将该对象置于value属性中,类本身提供了一些实用方法用来帮助对value进行判空,过滤等处理。简化了我们的开发。

优雅的剥洋葱

  说了这么多,让我们回到前言里那个问题吧,看看怎么用 Optional 类来优雅的剥洋葱。

image.png

  我们先构造一个符合这种格式的 Json 对象:

public static void main(String[] args) {
    JSONObject jsonData = JSON.parseObject(
        "{\"code\":200,\"msg\":\"success\",\"data\":{\"items\":[{\"value\":8},{\"value\":3}]}}");
    System.out.println(jsonData);
}

  假设现在我们请求某接口,得到的响应是上面的jsonData这个对象(这里我就不写接口了,简单模拟一下)。我们想得到 items 数组第一项的 value 标签值,可以用下面的写法来 get 想要的值。

Integer result = Optional.ofNullable(jsonData.getJSONObject("data"))
    .map(data -> data.getJSONArray("items"))
    .map(items -> items.getJSONObject(0))
    .map(item -> item.get("value"))
    .map(value -> value.toString())
    .map(Integer::valueOf)
    .orElse(0);
System.out.println(result);

  结果如下,符合预期

1f6bd60129000392da20a7324c6fc21.jpg

  可以看出,这一连串的 get get get,如果不用 Optional ,那代码简直没法看了,这里代码里并没有判空操作,大家感兴趣可以对这个初始化的字符串进行修改,让某一次 get 为 null,照样不会抛出 NPE,这里我就不演示了。当然,这里我 get 不到的处理是返回一个默认值 0,具体使用哪个方法处理还得看你自己的实际情况。

  我们来分析一下,为啥不用写判空代码了呢?

  首先是调用 ofNullable 方法,得到一个 Optional 对象,接着调用一连串的 map 方法,如果任何一次 get 的结果为 null,通过上一节对 map 方法源码的解读,方法一进来会调用if (!isPresent())来判断value是否为 null,如果是的话,直接return empty();,那么下一次再调用 map 时,检查if (!isPresent())仍然通过不了,还是return empty();。所以只要某次 get 不到值,整个 map,filter,flatMap这一系列调用链都是返回一个value为空的 Optional 对象,直到最后的 orElse 或者 orElseThrow 这种方法来处理。

Optional 的优缺点之我见

  Optional的优点不用说了,优雅,代码写出来链式调用,结构好看,不用大量的判空代码。不过个人感觉还是有一些缺点的:

  • 比较占空间,毕竟把原来的对象又包了一层,所以如果只是简单判断一个对象是否为null,个人感觉直接 if 判断可能更好一点,而且语义还更加清晰
  • 链式 map 的时候,无法得知具体哪一次 get 失败了

总结

  本文先简单解读了下Optional类的源代码,接着举例介绍了面对复杂对象如何用Optional来优雅的 get 到值,如果有理解的不对的地方,评论区友好讨论。

猜你喜欢

转载自juejin.im/post/7118904901023105061