Optional 的不正确实践

显式 if-else 本身合法。取得 Optional 中的 User 后,才能调用 User 的方法;Optional<User> 没有 getOrders():

1
2
3
4
5
6
Optional<User> user = findUser(id);
if (user.isPresent()) {
return user.get().getOrders();
} else {
return Collections.emptyList();
}

get() 在 empty 时抛出 NoSuchElementException;此例先检查存在性。如果只需要映射结果和缺省值,map(...).orElseGet(...) 可以减少显式解包。

Optional 主要用于可能无结果的方法返回值。用作 POJO 字段时需核对序列化、持久化与框架绑定契约,不因字段用了 Optional 就推导框架支持。

参数是否使用 Optional 取决于调用契约。调用者需要明确传入“缺席”时可以采用这种签名;普通必填参数直接使用领域类型,不额外包装。

Optional 表达有值或无值。布尔条件可直接分支;多种业务错误需要不同原因时采用结构化结果。存在多个返回路径并不构成禁止使用 Optional 的理由。

正确的用法

选对构造器

Optional.of(T value)

如果需要断言值不为空,使用这个构造器。

该方法通过一个非 null 的 value 来构造一个 Optional,返回的 Optional 包含了 value 这个值。对于该方法,传入的参数一定不能为 null,否则便会抛出 NullPointerException。

Optional.ofNullable(T value)

如果这个值可以为空,使用这个构造器。

该方法和 of 方法的区别在于,传入的参数可以为 null , 但是前面 javadoc 不是说 Optional 只能包含非 null 值吗?原来该方法会判断传入的参数是否为 null,如果为 null 的话,返回的就是 Optional.empty()。

Optional.empty()

empty() 表达无结果,也可以直接作为方法返回值。

empty() 返回不包含值的 Optional。API 表达有值与无值两种情况,不应依赖内部字段,也不能假设所有 empty 都有同一对象身份。

选对返回值

orElse 与 orElseGet 在缺席时提供值,区别在默认值的求值时机:传给 orElse 的表达式先求值,orElseGet 的 Supplier 仅在缺席时执行。map 映射普通值,flatMap 接续返回 Optional 的函数;collect 属于 Stream,Optional 没有该方法,也不采用 Stream 的终端操作分类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 存在即返回, 无则提供默认值
return user.orElse(null); //而不是 return user.isPresent() ? user.get() : null;
return user.orElse(UNKNOWN_USER);

// 存在即返回, 无则由函数来产生
return user.orElseGet(() -> fetchAUserFromDatabase()); //而不要 return user.isPresent() ? user: fetchAUserFromDatabase();

// map 转换存在的值,orElseGet 提供缺席时的默认结果。
return user.map(User::getOrders).orElseGet(Collections::emptyList);
// 当 getOrders() 保证非 null 时,下面分支与上一行等价。
if(user.isPresent()) {
return user.get().getOrders();
} else {
return Collections.emptyList();
}
// map 是可能无限级联的
return user.map(u -> u.getUsername())
.map(name -> name.toUpperCase())
.orElse(null);
// flatMap 方法与 map 方法的区别在于,map 方法参数中的函数 mapper 输出的是值,然后 map 方法会使用 Optional.ofNullable 将其包装为 Optional;而 flatMap 要求参数中的函数 mapper 输出的就是 Optional。

Optional<String> username = Optional
.ofNullable(getUserById(id))
.map(User::getUsername)
.map(name -> name.toLowerCase(Locale.ROOT));

System.out.println("Username is: " + username.orElse("Unknown"));

选对消费者

ifPresent 在有值时执行 Consumer,适合明确的副作用边界。它不会返回转换后的 Optional,也不提供通用 Continuation 的控制流能力。显式存在性分支同样合法,按分支的复杂度选择。

1
2
3
4
5
6
7
// 存在才对它做点什么
user.ifPresent(System.out::println);

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

Java 9 对Optional的增强

or方法

or 方法的作用是,如果一个 Optional 包含值,则返回自己;否则返回由参数 supplier 获得的 Optional

ifPresentOrElse

ifPresentOrElse 方法的用途是,如果一个 Optional 包含值,则对其包含的值调用函数 action,即 action.accept(value),这与 ifPresent 一致;与 ifPresent 方法的区别在于,ifPresentOrElse 还有第二个参数 emptyAction —— 如果 Optional 不包含值,那么 ifPresentOrElse 便会调用 emptyAction,即 emptyAction.run()

stream

stream 方法的作用就是将 Optional 转为一个 Stream,如果该 Optional 中包含值,那么就返回包含这个值的 Stream;否则返回一个空的 Stream(Stream.empty())。
Java 8 可通过过滤与解包取得所有存在的 User:

1
2
3
4
5
6
7
8
// 此处 getUserById 返回的是 Optional<User>
public List<User> getUsers(Collection<Integer> userIds) {
return userIds.stream()
.map(this::getUserById) // 获得 Stream<Optional<User>>
.filter(Optional::isPresent)// 去掉不包含值的 Optional
.map(Optional::get)
.collect(Collectors.toList());
}

Java 9 起可以使用 Optional.stream():

1
2
3
4
5
6
public List<User> getUsers(Collection<Integer> userIds) {
return userIds.stream()
.map(this::getUserById) // 获得 Stream<Optional<User>>
.flatMap(Optional::stream) // Stream 的 flatMap 方法将多个流合成一个流
.collect(Collectors.toList());
}

当前版本的复现与阅读

上述核心类型与 null 边界已在 JDK 21 复现:编译与运行入口、运行记录。反例检查 Optional 直接调用 getOrders 编译失败、of(null) 与 flatMap 返回 null 抛异常;正例检查存在/缺席、默认值求值与 Optional.stream 展平。getOrders() 非 null 时,示例分支与 map 链返回相同列表;实验另检查 getter 返回 null 时的差异:分支直接返回 null,map 将其变为缺席,再返回空列表。代码片段中的 User、findUser、getUserById 是业务上下文占位,完整实验自带最小 User 类型。

Maybe 与组合推导从函数式编程系列导读进入。Optional 的返回值定位及 map/flatMap 的 null 规则以 JDK 21 API 为准。本文保留原标题、日期和公开地址。

参考文献:

  1. 《使用 Java8 Optional 的正确姿势》
    2.《Java高级(三):Optional的巧用》