阅读 373

【评论抽掘金限定周边】| Java8的Optional你真的用对了吗?

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

前言

不管是一名小白程序员还是有三五年工作的CRUD BOY,或者是资深的高级Java工程师,在我们日常开发中,由于null的存在,经常会遇到NullPointerException,而我们为了避免该异常的产生,往往需要对于使用到的对象进行一些非空判断,最开始我们都会使用if...else来进行处理,比如下面的代码:

Person person = getPersonById("123");
if (person != null) {
    Address add = person.getAddress();
    if (add != null) {
        String city = add.getCity();
        if (city != null) {
            System.out.println(city);
        }
    }
}
复制代码

这种if判断的方式导致代码的阅读性和维护性都变差,并且很容易会忘记,导致出现BUG。

Java 8中推出了Optional<T>,专门来解决这一问题。我们先来看看官方文档的介绍。

A container object which may or may not contain a non-null value. If a value is present, isPresent() will return true and get() will return the value.

Additional methods that depend on the presence or absence of a contained value are provided, such as orElse() (return a default value if value not present) and ifPresent() (execute a block of code if the value is present).

This is a value-based class; use of identity-sensitive operations (including reference equality (==), identity hash code, or synchronization) on instances of Optional may have unpredictable results and should be avoided.

通过官方文档我们可以看出,Optional<T>是一个容器对象,容器中存放着一个值,这个值可能是空也有可能不是空,并且还提供了一系列依赖于值是否为空的方法;Optional<T>是一个value-based类,也可以理解为基于值的类,所以像==,hash,synchronization这些操作应该避免,因为这些操作一般都是基于对象的空间地址而并非值。

方法

接下来我们看一下Optional<T>都提供哪些方法。

创建Optional

Optional.empty()

返回一个空的 Optional实例。

public static<T> Optional<T> empty() {
    @SuppressWarnings("unchecked")
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
}
复制代码

Optional.of(T value)

返回具有 Optional的当前非空值的Optional

public static <T> Optional<T> of(T value) {
    return new Optional<>(value);
}
private Optional(T value) {
    this.value = Objects.requireNonNull(value);
}
复制代码

该方法传入对象必须不能为空,如果为空则会抛出空指针异常。

Optional.ofNullable(T value)

返回一个 Optional指定值的Optional,如果非空,则返回一个空的 Optional

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

ofNullable(T value)of(T value)唯一的区别是可以传入空对象,如果为空则同等于empty()

其他相关方法

isPresent

如果存在值返回 true,否则为 false

public boolean isPresent() {
return value != null;
}
复制代码

ifPresent

如果存在值,则使用该值调用指定的消费者,否则不执行任何操作。

public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}
复制代码

注意和isPresent()方法区分。

orElse

如果值存在返回值,否则返回 other

public T orElse(T other) {
    return value != null ? value : other;
}
复制代码

orElseGet

返回值(如果存在),否则调用 other并返回该调用的结果。

public T orElseGet(Supplier<? extends T> other) {
    return value != null ? value : other.get();
}
复制代码

orElseThrow

返回包含的值(如果存在),否则抛出由提供的exceptionSupplier创建的异常。

public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
    if (value != null) {
        return value;
    } else {
        throw exceptionSupplier.get();
    }
}
复制代码

map

将原Optional<T>转换为Optional<U>,如果值为空,则返回的也是空的Optional<U>

public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent())
        return empty();
    else {
        // map方法中将apply的结果包装为Optional
        return Optional.ofNullable(mapper.apply(value));
    }
}
复制代码

比如以下代码的目的是取出pOptional中person的address,如果person为空,则返回的的是Optional.empty(),反之则是Optional.ofNullable(person.getAddress())

Optional<Person> pOptional = Optional.ofNullable(getPersonById(123));
Optional<Address> address = pOptional.map(new Function<Person, Address>() {
    @Override
    public Address apply(Person person) {
        // 返回的是值
        return person.getAddress();
    }
});
复制代码

以上代码可简化为:

Optional<Address> add = pOptional.map(Person::getAddress);
复制代码

flatMap

从结果上看和map方法一样,都是将原Optional<T>转换为Optional<U>,就别在于方法参数中的mappermap()方法的mapper要求apply()方法返回的是具体的值,而flatMap()mapper参数的apply()方法返回的就是一个Optional<U>对象。

public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent())
        return empty();
    else {
        return Objects.requireNonNull(mapper.apply(value));
    }
}
复制代码

上述map()中的案例使用flatMap()实现如下:

Optional<Address> address1 = pOptional.flatMap(new Function<Person, Optional<Address>>() {
    @Override
    public Optional<Address> apply(Person person) {
        // 需要自己包装成Optional
        return Optional.of(person.getAddress());
    }
});
复制代码

同样可以简写为:

pOptional.flatMap(person1 -> Optional.of(person1.getAddress()));
复制代码

filter

对值进行过滤,如果值存在并且在predicate中返回为true,返回Optional本身,否则返回empty()

public Optional<T> filter(Predicate<? super T> predicate) {
    Objects.requireNonNull(predicate);
    if (!isPresent())
        return this;
    else
        return predicate.test(value) ? this : empty();
}
复制代码

比如以下案例:

Optional<Person> pOptional = Optional.ofNullable(getPersonById(123));
Optional<String> nameOpt = pOptional.filter(new Predicate<Person>() {
    @Override
    public boolean test(Person person) {
        return person.getAge() > 10;
    }
}).map(Person::getName);
System.out.println(nameOpt.orElse("Unknown"));
复制代码

简写为:

Optional<String> nameOpt = pOptional.filter(p -> p.getAge() > 10).map(Person::getName);
System.out.println(nameOpt.orElse("Unknown"));
复制代码

那么通过上面的方法你是不是想立马在代码中对于你的if...else进行改造了呢?回顾我们最开始的代码:

Person person = getPersonById("123");
if (person != null) {
    Address add = person.getAddress();
    if (add != null) {
        String city = add.getCity();
        if (city != null) {
            System.out.println(city);
        }
    }
}
复制代码

我们来用Optional改造一下看看:

Person person = getPersonById("123");
Optional<String> cityOption = Optional.ofNullable(person)
        .map(Person::getAddress)
        .map(Address::getCity);
System.out.println("city is :"+cityOption.orElse("Unknown city."));
复制代码

是不是感觉代码看着好像“高大上”了,但是这样是正确的吗?在这个过程中其实创建了很多的Optional对象出来,起码在空间上是更浪费的。

Optional正确的使用方式应该是什么呢?

Optional使用原则

Java语言的架构师Brian Goetz明确定义:

Optional is intended 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

有时候我们学会了一个东西之后就像立马使用,看什么代码都想套进去,有个例子说:有了一把锤子后,看啥都像钉子。

总想去敲两下。比如以下代码:

避免:

public String fetchStatus() {
    String status = ... ;
    return Optional.ofNullable(status).orElse("PENDING");
}
复制代码

而应该:

public String fetchStatus() {
    String status = ... ;
    return status == null ? "PENDING" : status;
}
复制代码

通过三目运算很简单的功能,非要用Optional,不光可读性变差,还额外构建了一个Optional对象。

不要将任何域对象声明为Optional

不要将任何方法的参数(尤其是setter)和构造方法的参数设置为Optional。

因为Optional不能被序列化,所以Optional设计时就没打算被用作对象的属性。

如果想通过Optional来保证对象的属性不被设置为null值,可以通过如下方式实现:

// PREFER
public class Customer {
    private final String name;     // 不能为空
    private final String postcode; // 可以为空
    public Cart(String name, String postcode) {
        this.name = Objects.requireNonNull(name, () -> "Name cannot be null");
        this.postcode = postcode;
    }

    public Optional<String> getPostcode() {
        return Optional.ofNullable(postcode);
    }
    ...
}
复制代码

可以看到这里getPostcode()方法返回了一个Optional,但是不要在你的代码里将所有getter都改成这样,尤其是返回集合或数组的情况,返回空集合和数组就可以了,因为集合本身就有empty()方法可以判断是否为空。

在方法参数中使用Optionalin是另一个常见错误。这将导致代码的复杂度变高。应该在方法内部对参数进行检查,而不是强制调用方使用Optional

不要使用Optional替代集合返回值

通过返回空集合的形式表示方法没有返回结果,通过Collections.emptyList()emptyMap()emptySet()构建空返回。

Optional值的比较使用equals

因为Optional类是一个value-based类,它的equals方法本身就是比较值,所以如果你需要对两个Optional中的值进行比较,则可以直接使用equals方法。

public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }
    if (!(obj instanceof Optional)) {
        return false;
    }
    Optional<?> other = (Optional<?>) obj;
    return Objects.equals(value, other.value);
}
复制代码

不要将Null值赋值为Optional变量

Optional.empty()会初始化一个Optional代替null值,Optional是一个容器,被定义为null就没有意义。

避免以下代码:

public Optional<Cart> fetchCart() {
    Optional<Cart> emptyCart = null;
    ...
}
复制代码

而应该:

public Optional<Cart> fetchCart() {
    Optional<Cart> emptyCart = Optional.empty();
    ...
}
复制代码

在调用 get() 之前确保 Optional 具有值

如果你需要调用get()方法获取Optional值时,一定要确保在调用之前检查过Optional中有值;通常都会使用isPresent()-get()搭配的方式使用,但是这种方式并不优雅;但是你如果一定要选择使用get()则不要忘记isPresent()

避免以下代码:

Optional<Cart> cart = ... ; // 可能返回一个空的Optional
...
// 如果cart是空的会抛出java.util.NoSuchElementException
Cart myCart = cart.get();
复制代码

而应该:

if (cart.isPresent()) {
    Cart myCart = cart.get();
    ... 
} else {
    ... 
}
复制代码

值为空时,通过 orElse() 方法返回默认对象

这种方式比isPresent()-get()方式更加优雅。但是这里有点小问题,就是即使Optional有值时,也会执行orElse()方法,并且需要构建一个对象,所以性能上有一点小小的损耗。

避免以下代码:

public static final String USER_STATUS = "UNKNOWN";
...
public String findUserStatus(long id) {

    Optional<String> status = ... ; // 可能返回一个空的Optional

    if (status.isPresent()) {
        return status.get();
    } else {
        return USER_STATUS;
    }
}
复制代码

而应该:

public static final String USER_STATUS = "UNKNOWN";

public String findUserStatus(long id) {

    Optional<String> status = ... ; // 可能返回一个空的Optional

    return status.orElse(USER_STATUS);
}
复制代码

值为空时,通过 orElseGet() 方法返回默认对象

orElseGet()是对isPresent()-get()的另一种优雅替代方案,同时比orElse() 来说,要更加高效,因为orElseGet()方法的参数是Java8中的Supplier,只有Optional中的值为空时,才会执行Supplier的get()方法,相对`orElse()没有性能损失。

避免以下代码:

public String computeStatus() {
    ... 
}

public String findUserStatus(long id) {

    Optional<String> status = ... ;

    if (status.isPresent()) {
        return status.get();
    } else {
        return computeStatus();
    }
}
复制代码

同样避免:

public String computeStatus() {
    ... 
}

public String findUserStatus(long id) {
    Optional<String> status = ... ; 
    // computeStatus() 在status不为空时也会调用
    return status.orElse(computeStatus()); 
}
复制代码

而应该:

public String computeStatus() {
    ... // some code used to compute status
}

public String findUserStatus(long id) {
    Optional<String> status = ... ; 
    // computeStatus()只有在status为空时调用
    return status.orElseGet(this::computeStatus);
}
复制代码

总结

乍一看Optional好像挺简单的,但是要想正确使用并不容易,OptionalAPI主要用于返回类型,清晰表达返回值中没有结果的可能性

  • 不要过度使用Optional
  • Optional尽量作为方法的返回值使用
  • 获取之前先判断是否有值
  • 适当使用Optional API

一些不恰当的使用可能导致在某些情况下出现BUG,建议收藏本文,使用Optional时进行参考。

抽奖说明

  • 本活动由掘金官方支持 详情可见掘力星计划

  • 欢迎在评论区讨论,掘金官方将在掘力星计划活动结束后,在评论区抽送100份掘金周边,抽奖详情见活动文章。

  • 与文章内容相关的评论、建议、讨论等,「踩踩」「学习了」等泛泛类的评论无法获奖

讨论话题都给大家准备好了,思考一下,下图中的这几个人有什么问题,以及最后提交的代码是否有BUG,可以在留言区讨论。

文章分类
后端
文章标签