review 的时候,我看到一段"用 Optional 写的判空"
5 月底做代码评审,看到同事写了这么一段:
Optional<User> userOpt = userMapper.findById(userId);
if (userOpt.isPresent()) {
User user = userOpt.get();
String name = user.getName();
...
} else {
throw new BizException("用户不存在");
}
Java 8 都发布五年了,Optional 还是被写成了 if (x != null) 的另一种写法,而且比直接判空还多两行。这不能怪他,我刚学 Optional 的时候也是这么写的,以为它就是个"官方的判空工具"。
后来我踩了个坑,才想明白 Optional 到底该怎么用。
orElse 里的代码,总会执行
那是做一个用户信息的本地缓存,我写的代码:
public User getUser(Long id) {
return Optional.ofNullable(localCache.get(id))
.orElse(loadFromDb(id)); // 缓存没有就查库
}
private User loadFromDb(Long id) {
log.info("查询数据库, id={}", id);
return userMapper.selectById(id);
}
上线之后 DBA 找过来了:用户库的 QPS 从 800 涨到 1720,翻了一倍多。查日志发现,缓存命中的时候也打了"查询数据库"。
原因很基础,但当时我完全没意识到:orElse 的参数是一个已经求值完毕的对象。Java 的方法调用是先算参数再进方法,所以 loadFromDb(id) 在 Optional.ofNullable(...) 执行之前就已经跑完了。Optional 内部的 orElse 只是二选一返回,它没法让已经执行的语句倒回去。
写个测试验证:
public class OptionalTrap {
static User loadFromDb(Long id) {
System.out.println("执行了 loadFromDb, id=" + id);
return new User(id, "张三");
}
public static void main(String[] args) {
User cached = new User(1L, "缓存里的李四");
System.out.println("--- orElse ---");
User u1 = Optional.of(cached).orElse(loadFromDb(1L));
System.out.println("--- orElseGet ---");
User u2 = Optional.of(cached).orElseGet(() -> loadFromDb(1L));
}
}
输出:
--- orElse ---
执行了 loadFromDb, id=1 ← 明明有值,还是执行了
--- orElseGet ---
orElseGet 的参数是 Supplier,是个 lambda,只有需要的时候才会被调用。看一眼源码就很清楚:
public T orElse(T other) {
return value != null ? value : other; // other 早就算好了
}
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get(); // 需要时才调用
}
改成 orElseGet 之后,用户库 QPS 从 1720 回到 810。我们缓存命中率 92%,那 920 次多余的查询就是这么来的。
判断标准其实很直观:orElse 的参数如果是常量或已经存在的对象,用 orElse;如果是一次方法调用、一次 RPC、一次数据库查询,一律用 orElseGet。
Optional 的正确位置:作为返回值
Optional 的设计意图在它的 javadoc 第一句就写明了:它是一个"可能为空的容器对象",主要用途是作为方法返回值,用来明确告诉调用方"这个方法可能没有结果"。
// 好:签名本身就说明了"可能查不到"
public Optional<User> findByMobile(String mobile) {
return Optional.ofNullable(userMapper.selectByMobile(mobile));
}
调用方看到这个签名,编译期就会被提醒要处理空的情况,比在注释里写一句"可能为 null"有效得多。
反过来,这几种用法都是不推荐的:
不要作为方法参数
// 不推荐
public void updateUser(Optional<User> userOpt) { ... }
调用方被迫先包装一层,而方法内部还是得 isPresent 判断,等于把处理成本转嫁给了调用方。而且 Optional 没有实现 Serializable,如果这是个 Dubbo 或者远程接口,序列化直接报错:
java.io.NotSerializableException: java.util.Optional
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
我们就有过一次,把 Optional<Order> 放进了 RPC 的返回 DTO 里,本地测试全过,一发布到测试环境就报这个错。
不要作为类的字段
同样的理由:不能序列化,而且 MyBatis、Jackson 这些框架对 Optional 字段的支持都很别扭。
不要在集合里放 Optional
// 毫无意义,集合本身就有"空"的概念
List<Optional<User>> users = ...;
链式判空:从 12 行到 6 行
这才是 Optional 真正好用的地方。改造前,取"订单所属用户的收货城市":
String city = "未知";
if (order != null) {
User user = order.getUser();
if (user != null) {
Address address = user.getAddress();
if (address != null) {
String c = address.getCity();
if (c != null && !c.isEmpty()) {
city = c;
}
}
}
}
用 Optional 之后:
String city = Optional.ofNullable(order)
.map(Order::getUser)
.map(User::getAddress)
.map(Address::getCity)
.filter(c -> !c.isEmpty())
.orElse("未知");
每一行对应一次可能的空值,任何一个环节返回 null,整条链就直接短路到 orElse,不会抛 NPE。读代码的时候视线一路往下,不用在大括号里数层级。
它的实现其实非常简单:map 会把函数的结果再包一层 Optional,函数返回 null 就得到 Optional.empty():
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)); // 结果可能是 empty
}
}
有个细节要注意:如果中间某一环本身返回的也是 Optional,要用 flatMap 而不是 map,否则会得到 Optional<Optional<T>>:
Optional<Address> findDefaultAddress(User user) { ... }
// 错:得到 Optional<Optional<Address>>,编译不过
Optional<Optional<Address>> bad = userOpt.map(u -> findDefaultAddress(u));
// 对:flatMap 会把两层拍平
Optional<Address> good = userOpt.flatMap(u -> findDefaultAddress(u));
还有几个要注意的
get() 之前一定要确认有值
Optional.get() 在值为空时抛的是 NoSuchElementException,比 NPE 还难排查,因为堆栈里看不出是哪个字段空了:
java.util.NoSuchElementException: No value present
at java.util.Optional.get(Optional.java:135)
用 orElse、orElseGet、orElseThrow 替代 get()。orElseThrow 在找不到值要抛业务异常时特别合适:
User user = findByMobile(mobile)
.orElseThrow(() -> new BizException("用户不存在: " + mobile));
别用 of() 包可能为 null 的值
Optional.of(null) 直接抛 NPE。不确定是不是 null 就用 ofNullable。我们有个地方从 Map 里取值然后 Optional.of(map.get(key)),key 不存在的时候直接炸了。
性能:业务代码放心用,极致热路径要留意
Optional 是个对象,每次 map 都要创建新的 Optional 实例加一次函数调用。用 JMH 跑 1000 万次链式取值:
| 写法 | 平均单次 |
|---|---|
| 嵌套 if 判空 | 0.8 ns |
| Optional 链(4 层) | 2.1 ns |
差 2.6 倍,但绝对值都是纳秒级。业务代码里一次请求调几十次,多出来的开销是几百纳秒,完全无所谓。我们的规则是:业务代码随便用,自己写的基础工具类(每秒可能被调几十万次的那种)用回 if。
JDK 9 才有的几个方法
ifPresentOrElse、or、stream 这几个是 JDK 9 加的。我们项目还是 JDK 8,看到网上的例子用了这些方法,编译不过别以为是自己写错了。
小结
orElse的参数会被无条件求值,只要里面是方法调用、RPC、数据库查询,就要换成orElseGet。这是我踩过的最实在的一个坑。- Optional 主要用作方法返回值,表达"可能没有结果"。不要用作参数、字段、集合元素,它不可序列化。
- 它的价值在链式调用:
map串起来的每一层都是一次空值短路,替代层层嵌套的 if。中间返回 Optional 的方法要用flatMap。 - 别用
isPresent()+get()那套,那是把 Optional 用成了更啰嗦的判空。
我现在的习惯是:写查询方法时如果结果可能为空,返回值就声明成 Optional,逼着调用方处理。我们重构过的几个模块里,NPE 类的线上问题从每月四五次降到了几乎没有。