Java 中的 Optional 是不是鸡肋?

Optional<User> userOption = Optional.ofNullable(userService.getUser(...));
if (!userOption.isPresent()) {....}

if 里面还有 Optional 套 Optional ,连环判断 isPresent 的。

关于 Optional 老早之前我就看到很多争论,有好多怒喷 Optional 鸡肋,是个糟糕的设计,巴拉巴拉。

先抛开这些不管,反正如果平日是按照以上的用法来用 Optional 的,还是直接用 if(user != null){....} 判空算了,何必包一层 Optional,再判断呢?这样使用 Optional 是不对滴,画蛇添足。

那 Optional 应该如何用呢?

Optional 的真实执行逻辑是否与你所想的一样?

今天我们就深入源码看看。

我们先来看看 Optional 设计出来的意图是什么, Java 语言架构师 Brian Goetz 是这么说的:

Our intention was to provide a limited mechanism for library method return types where there needed to be a clear way to represent “no result”, and using null for such was overwhelmingly likely to cause errors.

意思就是:Optional 可以给返回结果提供了一个表示无结果的值,而不是返回 null。

简单理解下,Optional 其实就是一个壳,里面放着原先的值,至于这个值是不是 null 另说,反正拿到的这个壳肯定不是 null。

图片

网上比较流行的说法是 Optional 可以避免空指针,我不太赞同这种说法。因为最终的目的是拿到 Optional 里面存储的值,如果这个值是 null ,不做额外的判断,直接使用还是会有空指针的问题。

我认为 Optional 的好处在于可以简化平日里一系列判断 null 的操作,使得用起来的时候看着不需要判断 null,纵享丝滑,表现出来好像用 Optional 就不需要关心空指针的情况。

而事实上是 Optional 在替我们负重前行,该有的判断它替我们完成了,而且用了 Optional 最后拿结果的时候还是小心的,盲目 get 一样会抛错,Brian Goetz 说 get 应该叫 getOrElseThrowNoSuchElementException。

我们来看一下代码就很清楚 Optional 的好处在哪儿了。比如现在有个 yesSerivce 能 get 一个 Yes,此时需要输出 Yes 所在的省,此时的代码是这样的:

Yes yes = getYes();
if (yes != null) {
    Address yesAddress = yes.getAddress();
    if (yesAddress != null) {
        Province province = yesAddress.getProvince();
        System.out.println(province.getName());
    }
}
throw new NoSuchElementException(); //如果没找到就抛错

如果用 Optional 的话,那就变成下面这样:

Optional.ofNullable(getYes())
        .map(a -> a.getAddress())
        .map(p -> p.getProvince())
        .map(n -> n.getName())
        .orElseThrow(NoSuchElementException::new);

可以看到,如果用了 Optional,代码里不需要判空的操作,即使 address 、province 为空的话,也不会产生空指针错误,这就是 Optional 带来的好处!

说到这,我想提个问:

如果在 a.getAddress() 时拿不到值的话,你说是会继续执行map(p -> p.getProvince()) 还是直接跳到 orElseThrow? 或者反过来如果 map(n -> n.getName()) 不为空,你说 orElseThrow 这个方法会不会执行?

接下来我们就来看下源码,看看 Optional 的实现机制。

Optional 源码

Optional 的代码十分简短且简单,如果去掉注释,我估计就100来行。

来看下几个关键的成员变量:

图片

符合前面提到的:Optional 就是个壳,里面的 value 才是正主。并且内置了一个 EMPTY 对象,用来替换当 value 为 null 时候的壳。

现在看下上面演示的 map 方法,看看它的内部实现是如何让我们不需要做非空判断的。

图片

可以看到很简单,没几行代码,我把方法中的两个调用实现都贴上去,这样对着看应该会更清晰:

图片

先判断 value 是否为空,如果是空的话说明真正要是值是空的,此时直接返回一个 empty(),还记得上面的 empty 方法吧?直接方式事先创建的空 Optional 。

如果 value 不为空,那说明值是存在的,因此调用 mapper (就是上面我们写的 a.getAddress 之类的)来操作一波这个 value,并且用 Optional.ofNullable 包了一层,这个方法内部也看到了,如果 value 是空的话,也是返回空 Optional,否则就利用 of 包裹 value 成 Optional 返回。

因此,不论你 Optional 里面到底有没有值,我 map 都能处理!如果你是空,我就返回空 Optional ,如果你有值,ok 我包裹成 Optional 返回,反正不论怎样,调用 map 的返回值都会是一个 Optional,而不是 null,所以执行时不会产生空指针的情况。

还记得上面的提问吗?结合 map 的源码,现在来回答下上面的问题,看注释:

图片

截个 orElseThrow 的实现,就是判断下 value ,如果是 null 就抛错。

图片

结合源码我们知道了答案:即使 Optional.ofNullable 返回的是空 Optional ,下面的 map 逻辑还是会执行,不会因为中间得到空值而直接跳到orElseThrow执行,这和我们平日知晓的 if else 逻辑不太一样,不为空orElseThrow也一样会执行,就是判断 value!= null然后直接返回 value 的值了。

好了,逻辑就是这么简单!上面之所以说是 Optional 在替我们负重前行,是因为该有的判断一个都没少,只是它替我们做了而已。

关于  Optional 还有个性能问题,我们看一下:

Optional 里有 orElseGet  和 orElse 这两个看起来挺相似的方法,都是处理当值为 null 时的兜底逻辑。可能你也在一些文章上看到说用 orElseGet 不要用 orElse ,因为在 Optional 有值时候 orElse 仍然会调用方法,所以后者性能比较差。其实从上面分析我们知道不论 Optional 是否有值,orElse 和 orElseGet  都会被执行,所以是怎么回事呢?

看下这个代码:

图片

这样看来 orElse 确实性能会差,奇怪了,难道是 bug?

我们来看下源码。

图片

可以看到两者的入参不同,一个就是普通参数,一个是 Supplier。我们已经得知不论Optional.ofNullable 返回的是否是空 Optional,下面的逻辑还是会执行,所以 orElse 和 orElseGet 这两个方法无论如何都会执行。

因此 orElse(createYes()) 会被执行,在参数入栈之前,执行了 createYes 方法得到结果,然后入栈,而 orElseGet 的参数是 Supplier,所以直接入栈,然后在调用 other.get 的时候,createYes 方法才会被触发执行,这就是两者的区别之处。

所以才会造成上面表现出的性能问题,因此不是 BUG,也不是有些文章说的 Optional 有值 orElse 也会被执行而 orElseGet 不会执行这样不准确的说法,相信现在你的心里很有数了。

既然都讲到这了,把 Optional 剩下几个方法讲讲完吧,没几个了。

来看个 of 和 ofNullable 的对比,看下注释应该很清晰了。

图片

再来看个 isPresent() 和 ifPresent(Consumer<? super T> consumer),两者名字有细微的差别,is 和 if。

图片

还有个 get,这个方法要小心,如果没做好判断,直接调用,当是空 Optional 时会抛错的。

图片

还有个 filter逻辑 和 map 的差不多,用于过滤数据,平日基本是就是先 filter 再 map,属于基操。

图片

还有个 flatMap ,这个和 map 逻辑一模一样,就入参有点不一样,用在返回值不是普通对象,是 Optional 包裹的对象的场景。

图片

这里又得提一点了,关于 POJO 里面的属性是否应该被 Optional 包裹,或者说是否应该把 get 方法包裹成 Optional 返回,类似下面这样的代码。

图片

在 stackoverflow 有个类似的提问。

图片

Brian Goetz 给了回答,我直接翻译了:你可能永远不应该将它用于返回结果数组或结果列表的内容,而应该返回空数组或空列表。你几乎不应该将它用作某个字段或方法参数,我认为经常使用它作为 getters 的返回值肯定是过度使用。

下面也有一堆不服的,说这发言更像是您自己所认为的,而没有什么依据表明这样用有什么不好,总之神仙打架瑟瑟发抖。

好了,把 Optional 的方法都讲完了,可以看到还是很简单的,也没有什么骚操作,比看并发包的简单多了。

总结下来 Optional 主要是简化一系列判空操作,执行过程是一条龙走到底的,你有几个 filter 和 map 不论得到的值空不空,都是执行到底包括 orElse 的逻辑。

再提一个题外话,在 oracle 官网上我看到一篇关于 Optional 的文章,上面写道:

像 Groovy 是利用 ?. 来避免判空的,例如这个代码:

String version = computer?.getSoundcard()?.getUSB()?.getVersion();

后面写了个 note: 请注意,它很快也将被包含在c#中,它曾被提议用于Java SE 7,但没有在那个版本中实现。

原创文章,作者:guozi,如若转载,请注明出处:https://www.sudun.com/ask/90140.html

(0)
guozi's avatarguozi
上一篇 2024年6月5日 上午11:56
下一篇 2024年6月5日 上午11:59

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注