前言

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

优雅的剥洋葱:浅析 Java Optional 类_构造方法

image.png

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

优雅的剥洋葱:浅析 Java Optional 类_包装类_02

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​​ 类来优雅的剥洋葱。

优雅的剥洋葱:浅析 Java Optional 类_包装类_03

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);
复制代码

结果如下,符合预期

优雅的剥洋葱:浅析 Java Optional 类_构造方法_04

image.png

可以看出,这一连串的 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 失败了