Java8 Optional 几个常见错误用法

栏目: 编程语言 · Java · 发布时间: 7年前

内容简介:Java 8 引入的 Optional 类型,基本是把它当作 null 值优雅的处理方式。其实也不完全如此,Optional 在语义上更能体现有还是没有值。所以它不是设计来作为 null 的替代品,如果方法返回 null 值表达了二义性,没有结果或是执行中出现异常。在 Oracle  做  Java 语言工作的  Brian Goetz 在 Stack Overflow 回复说的是  Optional 提供了一个有限的机制让类库方法返回值清晰的表达有与没有值,避免很多时候 null 造成的错误。并非有了 

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 来表示这个意义。

Java 8 于  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 字段或方法参数时就变得复杂了,需要进行两番检查。

public class User {
    private String firstName;
    private Optional<String> middleName = Optional.empty();
    private String lastName;
 
    public void setMiddleName(Optional<String> middleName) {
        this.middleName = middleName;
    }
 
    public String getFullName() {
        String fullName = firstName;
        if(middleName != null) {
            if(middleName.isPresent()){
                fullName = fullName.concat("." + middleName.get());
        }
 
        return fullName.concat("." + lastName);
    }
}

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

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

user.setMiddleName(Optional.empty());
user.setMiddleName(Optional.of("abc"));

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

    private String middleName;
 
    public void updateMiddleName(Optional<String> middleName) {
        if(middleName != null) {
            this.middleName = middleName.orElse(null);
        } else {
            this.middleName = null;
        }
    }

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

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

利用 Optional 过度检查方法参数

这一 Optional 的用法与之前的可能为 null 值的方法参数,不分清红皂白就用 if...else 检查,总有一种不安全感,步步惊心,结果可能事与愿违。

public User getUserById(String userId) {
    if(userId != null) {
        return userDao.findById(userId);
    } else {
        return null;
    }
}

只是到了 Java 8 改成了用 Optional

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

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

userDao.findById(...)
getUserById(userId)

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

public User getUserById(String userId) { //抛出出 NullPointerException 如果 null userId
    return userDao.findById(Objects.requireNoNull(userId, "Invalid null userId");
}
 
//or
public User getUserById(String userId) { //抛出 IllegalArgumentException 如果 null userId
    Preconditions.checkArgument(userId != null, "Invalid null userId");
    return userDao.findById(userId);
}

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

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

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

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

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

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

#1 和 #2 中应用 flatMap 再次用 Optional.ofNullable() 是因为担心 order.getProduct()product.getId() 返回了 null 值,所以又用 Optional.ofNullable(...) 包裹了一次。代码的执行结果仍然是对的,代码真要这么写的话真是 Oracle 的责任。这忽略了 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));
        }
    }

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

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

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

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

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

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

    public static <T> Optional<T> of(T value) {
        return new Optional<>(value);
    }
 
    public static <T> Optional<T> ofNullable(T value) {
        return value == null ? empty() : of(value);
    }

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

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

小结:

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

链接:

  1. Java 8 Optional use cases

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Programming Python

Programming Python

Mark Lutz / O'Reilly Media / 2006-8-30 / USD 59.99

Already the industry standard for Python users, "Programming Python" from O'Reilly just got even better. This third edition has been updated to reflect current best practices and the abundance of chan......一起来看看 《Programming Python》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

MD5 加密
MD5 加密

MD5 加密工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具