前言
杨宗纬唱过一首歌,叫做《洋葱》,里面有一句是:“如果你愿意一层一层一层的剥开我的心。。。”,歌曲是非常的感人。
其实在咱们程序员日常开发中,也经常会遇见需要一层一层剥洋葱的情况,如下图:
比方说,现在咱们在后端需要请求某个接口,该接口返回格式如图,咱们需要得到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
类来优雅的剥洋葱。
我们先构造一个符合这种格式的 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);
复制代码
结果如下,符合预期
可以看出,这一连串的 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 到值