Optional 类的使用

发布时间 2023-12-11 09:47:20作者: 红豆绿豆abc

Optional 类的使用

引言

因为 java 的 stream api 的终止操作符可能会返回 Optional 对象,所以研究一下 Optional 类


Optional 概念

看之前代码中 Optional​ 的使用:

Optional<JdPromotionDiscountInfoBO> jdPromotionOptional = discountInfos.stream().filter(x -> promotionType.equals(x.getPromotionType().toString()+"_"+x.getUser())).findFirst();
                    if (jdPromotionOptional.isPresent()) {
                        JdPromotionDiscountInfoBO jdPromotionDiscountInfoBO = jdPromotionOptional.get();
                        BigDecimal value = new BigDecimal(jdPromotionDiscountInfoBO.getPromotionValue());
                        String promotionValue = value.toString();
                        values.add(promotionValue);
                    } else {
                        values.add("0");
                    }

关于 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.

我们的目的是为库方法返回类型提供一种有限的机制,其中需要有一种明确的方式来表示“无结果”,而使用 null 极有可能会导致错误。

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

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

image

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

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

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

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

        UserService userService = new UserService();
        User user = userService.getUser();
        if (user != null) {
            Address userAddress = user.address();
            if (userAddress != null) {
                Province province = userAddress.province();
                System.out.println(province.name());
            }
        }
        throw new NoSuchElementException(); //如果没找到就抛错

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

        Optional.ofNullable(userService.getUser())
                .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 就是个壳,里面的 value 才是正主。并且内置了一个 EMPTY 对象,用来替换当 value 为 null 时候的壳。

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

图片

把方法中的两个调用实现都贴上去,这样对着看应该会更清晰:

图片

先判断 value 是否为空,如果是空的话说明真正要是值是空的,此时直接返回一个 empty()​,这个方法直接返回事先创建的空 Optional 对象。

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

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

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

image

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

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

所以说是 Optional 在替我们负重前行,是因为该有的判断一个都没少,只是它替我们做了而已。

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

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

看下这个代码:

image

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

我们来看下源码。

image

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

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

所以才会造成上面表现出的性能问题,因此不是 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 的返回值肯定是过度使用。

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

不过我个人倾向于 Brian Goetz,我觉得 Optional 的用处就是逻辑处理的时候避免判空,仅此而已,所以 POJO 本该如何还是如何,Optional 应该交由逻辑处理代码来用。

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

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

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

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

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

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

咱也不知道为啥没被接受,反正我觉得上面这写法挺清爽的。

总结

直白的讲, 当我们还在以如下几种方式使用 Optional 时, 就得开始检视自己了

  1. 调用 isPresent()​ 方法时
  2. 调用 get()​ 方法时
  3. Optional 类型作为类/实例属性时
  4. Optional 类型作为方法参数时

isPresent()​ 与 obj != null​ 无任何分别, 我们的生活依然在步步惊心. 而没有 isPresent()​ 作铺垫的 get()​ 调用在 IntelliJ IDEA 中会收到告警

Reports calls to java.util.Optional.get() without first checking with a isPresent() call if a value is available. If the Optional does not contain a value, get() will throw an exception. (调用 Optional.get() 前不事先用 isPresent() 检查值是否可用. 假如 Optional 不包含一个值, get() 将会抛出一个异常)

把 Optional 类型用作属性或是方法参数在 IntelliJ IDEA 中更是强力不推荐的

Reports any uses of java.util.Optional, java.util.OptionalDouble, java.util.OptionalInt, java.util.OptionalLong or com.google.common.base.Optional as the type for a field or a parameter. Optional was designed to provide a limited mechanism for library method return types where there needed to be a clear way to represent "no result". Using a field with type java.util.Optional is also problematic if the class needs to be Serializable, which java.util.Optional is not. (使用任何像 Optional 的类型作为字段或方法参数都是不可取的. Optional 只设计为类库方法的, 可明确表示可能无值情况下的返回类型. Optional 类型不可被序列化, 用作字段类型会出问题的)

所以 Optional 中我们真正可依赖的应该是除了 isPresent()​ 和 get()​ 的其他方法:

  1. public<U> Optional<U> map(Function<? super T, ? extends U> mapper)
  2. public T orElse(T other)
  3. public T orElseGet(Supplier<? extends T> other)
  4. public void ifPresent(Consumer<? super T> consumer)
  5. public Optional<T> filter(Predicate<? super T> predicate)
  6. public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper)
  7. public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X

我略有自信的按照它们大概使用频度对上面的方法排了一下序.

先又不得不提一下 Optional 的三种构造方式: Optional.of(obj)​, Optional.ofNullable(obj)​ 和明确的 Optional.empty()

Optional.of(obj)​: 它要求传入的 obj 不能是 null 值的, 否则还没开始进入角色就倒在了 NullPointerException​ 异常上了.

Optional.ofNullable(obj)​: 它以一种智能的, 宽容的方式来构造一个 Optional 实例. 来者不拒, 传 null 进到就得到 Optional.empty()​, 非 null 就调用 Optional.of(obj)​.

现在才开始怎么去使用一个已有的 Optional 实例, 假定我们有一个实例 Optional<User> user​, 下面是几个普遍的, 应避免 if(user.isPresent()) { ... } else { ... }​ 几中应用方式.

存在即返回, 无则提供默认值

return user.orElse(null);  //而不是 return user.isPresent() ? user.get() : null;
return user.orElse(UNKNOWN_USER); 

存在即返回, 无则由函数来产生

return user.orElseGet(() -> fetchAUserFromDatabase()); //而不要 return user.isPresent() ? user: fetchAUserFromDatabase();

存在才对它做点什么

user.ifPresent(System.out::println);
 
//而不要下边那样
if (user.isPresent()) {
  System.out.println(user.get());
}

map 函数隆重登场

user.isPresent()​ 为真, 获得它关联的 orders​, 为假则返回一个空集合时, 我们用上面的 orElse​, orElseGet​ 方法都乏力时, 那原本就是 map​ 函数的责任, 我们可以这样一行

return user.map(u -> u.getOrders()).orElse(Collections.emptyList())
 
//上面避免了我们类似 Java 8 之前的做法
if(user.isPresent()) {
  return user.get().getOrders();
} else {
  return Collections.emptyList();
}

map​ 是可能无限级联的, 比如再深一层, 获得用户名的大写形式

return user.map(u -> u.getUsername())
           .map(name -> name.toUpperCase())
           .orElse(null);

这要搁在以前, 每一级调用的展开都需要放一个 null 值的判断

User user = .....
if(user != null) {
  String name = user.getUsername();
  if(name != null) {
    return name.toUpperCase();
  } else {
    return null;
  }
} else {
  return null;
}

针对这方面 Groovy 提供了一种安全的属性/方法访问操作符 ?.

user?.getUsername()?.toUpperCase();

Swift 也有类似的语法, 只作用在 Optional 的类型上.

用了 isPresent()​ 处理 NullPointerException 不叫优雅, 有了 orElse, orElseGet 等, 特别是 map​ 方法才叫优雅.

其他几个, filter()​ 把不符合条件的值变为 empty()​, flatMap()​ 总是与 map()​ 方法成对的, orElseThrow()​ 在有值时直接返回, 无值时抛出想要的异常.

一句话小结: 使用 Optional​ 时尽量不直接调用 Optional.get()​ 方法, Optional.isPresent()​ 更应该被视为一个私有方法, 应依赖于其他像 Optional.orElse()​, Optional.orElseGet()​, Optional.map()​ 等这样的方法.

最后, 最好的理解 Java 8 Optional 的方法莫过于看它的源代码 java.util.Optional, 阅读了源代码才能真真正正的让你解释起来最有底气, Optional 的方法中基本都是内部调用 isPresent()​ 判断, 真时处理值, 假时什么也不做.