Java8 Optional 几个常见错误用法

Java 8 引入的 Optional 类型,基本是把它当作 null 值优雅的处理方式。其实也不完全如此,Optional 在语义上更能体现有还是没有值。所以它不是设计来作为 null 的替代品,如果方法返回 null 值表达了二义性,没有结果或是执行中出现异常。

在 Oracle  做  Java 语言工作的  Brian Goetz 在 Stack Overflow 回复  Should Java 8 getters return optional type? 中讲述了引入  Optional 的主要动机。

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 就要完全杜绝 NullPointerException。

在 Java 8 之前一个实践是方法返回集合或数组时,应返回空集合或数组表示没有元素; 而对于返回对象,只能用 null 来表示不存在,现在可以用  Optional 来表示这个意义。

自 Java8 于  2014-03-18 发布后已 5 年有余,这里就列举几个我们在项目实践中使用 Optional 常见的几个用法。

Optional 类型作为字段或方法参数

这儿把 Optional  类型用为字段(类或实例变量)和方法参数放在一起来讲,是因为假如我们使用 IntelliJ IDEA 来写 Java 8 代码,IDEA 对于  Optional 作为字段和方法参数会给出同样的代码建议:

Reports any uses of java.util.Optional<T>, java.util.OptionalDouble, java.util.OptionalInt, java.util.OptionalLong or com.google.common.base.Optional as the type for a field or 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 是不可被序列化的,如果类是可序列化的就会出问题。

上面其实重复了 Java 8 引入  Optional 的意图,我们还有必要继续深入理解一下为什么不该用  Optional 作为字段或方法参数。

当我们选择 Optional 类型而非内部包装的类型后,应该是假定了该 Optional 类型不为 null,否则我们在使用 Optional 字段或方法参数时就变得复杂了,需要进行两番检查。

 1public class User {
 2    private String firstName;
 3    private Optional<String> middleName = Optional.empty();
 4    private String lastName;
 5
 6    public void setMiddleName(Optional<String> middleName) {
 7        this.middleName = middleName;
 8    }
 9
10    public String getFullName() {
11        String fullName = firstName;
12        if(middleName != null) {
13            if(middleName.isPresent()){
14                fullName = fullName.concat("." + middleName.get());
15        }
16
17        return fullName.concat("." + lastName);
18    }
19}

由于  middleName 的 setter 方法,我们可能造成 middleName 变为 null 值,所以在构建 fullName 时必须两重检查。

并且在调用 setMiddleName(...) 方法时也有些累赘了

1user.setMiddleName(Optional.empty());<br/>
2user.setMiddleName(Optional.of("abc"));

而如果字段类型非 Optional 类型,而传入的方法参数为 Optional 类型,要进行赋值的话

1    private String middleName;
2
3    public void updateMiddleName(Optional<String> middleName) {
4        if(middleName != null) {
5            this.middleName = middleName.orElse(null);
6        } else {
7            this.middleName = null;
8        }
9    }

前面两段代码如果应用 Optional.ofNullable(...) 包裹 Optional 来替代 if(middleName != null) 就更复杂了。

对于本例直接用 String 类型的 middleName  作为字段或方法参数就行,null 值可以表达没有 middleName。如果不允许 null 值  middleName, 显式的进行入口参数检查而拒绝该输入 -- 抛出异常。

利用 Optional 过度检查方法参数

这一 Optional 的用法与之前的可能为 null 值的方法参数,不分清红皂白就用  if...else 检查,心中毫无安全感,步步惊心,结果可能事与愿违。类似于只要听人说桃子与西瓜不能一起的传言,不去求证,而是宁可信其有,不可信可无,不管真与假,反正不一起吃肯定不会死一样。对于绝对不会 null 的值就不必用 Optional 来使自己得到安心。

1public User getUserById(String userId) {
2    if(userId != null) {
3        return userDao.findById(userId);
4    } else {
5        return null;
6    }
7}

只是到了 Java 8 改成了用 Optional

1    return if(Optional.ofNullable(userId)
2        .map(id -> userDao.findById(id))
3        .orElse(null);

上面两段代码其实是同样的问题,如果输入的 userId 是 null 值不调用 findById(...) 方法而直接返回 null 值,这就有两个问题

  1. 返回 null 时到底是传入了 null 值 userId 还是 userDao.findById(...) 返回了 null 值
  2. getUserById(userId) 的调用约定来说到底能不能接受 null 值 userId?如果不允许应该直接拒绝调用,否则返而隐藏了潜在的 bug

这种情况下立即抛出 NullPointerException 是一个更好的主意,参考下面的代码

1public User getUserById(String userId) { //抛出出 NullPointerException 如果 null userId
2    return userDao.findById(Objects.requireNoNull(userId, "Invalid null userId");
3}
4
5//or
6public User getUserById(String userId) { //抛出 IllegalArgumentException 如果 null userId
7    Preconditions.checkArgument(userId != null, "Invalid null userId");
8    return userDao.findById(userId);
9}

即使用了 Optional 的 orElseThrow 抛出异常也不能明确异常造成的原因,比如下面的代码

1public User getUserById(String userId) {
2    return Optional.ofNullable(userId)
3        .map(id -> userDao.findById(id))
4        orElseThrow(() ->
5            new RuntimeException("userId 是 null 或 findById(id) 返回了 null 值"));
6}

纠正办法是认真的审视方法的输入参数,对不符合要求的输入应立即拒绝,防止对下层的压力与污染,并报告出准确的错误信息,以有利于快速定位修复。

Optional.map(...) 中再次 null 值判断

假如有这样的对象导航关系 user.getOrder().getProduct().getId(), 输入是一个  user 对象

1    String productId = Optional.ofNullable(user)
2        .map(User::getOrder)
3        .flatMap(order -> Optional.ofNullable(order.getProduct()))  //1
4        .flatMap(product -> Optional.ofNullable(product.getId()))   //2
5        .orElse("");

#1 和 #2 中应用 flatMap 再次用 Optional.ofNullable() 是因为担心 order.getProduct() 或 product.getId() 返回了 null 值,所以又用 Optional.ofNullable(...) 包裹了一次。代码的执行结果仍然是对的,代码真要这么写的话真是 Oracle 的责任。这忽略了 Optional.map(...) 的功能,只要看下它的源代码就知道

1    public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
2        Objects.requireNonNull(mapper);
3        if (!isPresent())
4            return empty();
5        else {
6            return Optional.ofNullable(mapper.apply(value));
7        }
8    }

map(...) 函数中已有考虑拆解后的 null 值,因此呢 flatMap 中又 Optional.ofNullable 是多余的,只需简单一路用 map(...) 函数

1    String productId = Optional.ofNullable(user)
2        .map(User::getOrder)
3        .map(order -> order.getProduct())  //1
4        .map(product -> product.getId())   //2
5        .orElse("");

Optional.ofNullable 应用于明确的非  null 值

如果有时候只需要对一个明确不为 null 的值进行 Optional 包装的话,就没有必要用 ofNullable(...) 方法,例如

1public Optional<User> getUserById(String userId) {
2    if("ADMIN".equals(userId)) {
3        User adminUser = new User("admin");
4        return Optional.ofNullable(adminUser); //1
5    } else {
6        return userDao.findById(userId);
7    }
8}

在代码 #1 处非常明确 adminUser 是不可能为 null 值的,所以应该直接用 Optional.of(adminUser)。这也是为什么 Optional 要声明 of(..) 和 ofNullable(..) 两个方法。看看它们的源代码:

1    public static <T> Optional<T> of(T value) {
2        return new Optional<>(value);
3    }
4
5    public static <T> Optional<T> ofNullable(T value) {
6        return value == null ? empty() : of(value);
7    }

知道被包裹的值不可能为 null 时调用 ofNullable(value) 多了一次多余的 null 值检查。相应的对于非 null 值的字面常量

1Optional.ofNullable(100);  //这样不好
2Optional.of(100);          //应该这么用

小结:

  1. 要理解 Optional 的设计用意,所以语意上应用它来表达有/无结果,不适于作为类字段与方法参数
  2. 倾向于方法返回单个对象,用 Optional 类型表示无结果以避免 null 值的二义性
  3. Optional 进行方法参数检查不能掩盖了错误,最好是明确非法的参数输入及时抛出输入异常
  4. 对于最后两种不正确的用法应熟悉 Optional 的源代码实现就能规避

链接:

  1. Java 8 Optional use cases
永久链接 https://yanbin.blog/java8-optional-several-common-incorrect-usages/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。