尽管存在争议,Optional 极大地改善了 Java 应用程序的设计。在本文中,我们将探讨如何、何时以及在何处最佳地应用 Optional。
Justin Albano 2020-06-16
引言
Optional 类的引入是 Java 语言设计上的一大改进,但这一改进颇具争议。在 Optional 出现之前,许多开发者要么使用 null,要么抛出异常来表示期望的值不存在;而通过使用 Optional 类,我们可以明确地表达某个值可能存在,也可能不存在。尽管如此,如果 Optional 被不当使用,反而可能带来更多问题。
在本文中,我们将探讨 Optional 类的基础知识,包括:
- 引入它的目的及其背后的设计思路
Optional类中包含的核心方法- 何时何地适合使用
Optional - 可替代的其他技术方案
目的
Java 和大多数面向对象(OO)语言一样,在其类型系统中存在一个“隐秘后门”:任何非基本类型都可以被赋值为 null。
例如,假设我们有如下方法签名:
public Foo doSomething();
很明显,该方法返回一个 Foo 类型的对象,但它也可以返回另一个值:null。由于任何非基本类型都可以设为 null,开发者完全可以写出如下实现:
public Foo doSomething() {
return null;
}
空值(Nullity)问题
这给调用该方法的客户端带来了沉重负担。在使用从 doSomething() 返回的 Foo 对象之前,客户端必须先检查它是否为 null:
Foo foo = doSomething();
if (foo == null) {
// 处理 null 情况...
} else {
// 使用 foo 对象...
}
这种做法可以避免 NullPointerException(NPE),但也带来新问题:任何非基本类型对象都可能隐式地为 null。因此,理论上我们必须对每个方法返回的对象都进行空值检查。这不仅在大型应用中不可行,还会使代码变得混乱。例如,大量空值检查会引入样板代码,掩盖业务逻辑的清晰性。
根本问题在于:我们无法知道某个方法是否“有意”返回 null(比如找不到所需值时),还是“保证永不返回 null”。由于不确定,我们只能假设所有返回对象都可能是 null。
一种常见解决方案是使用 JavaDoc 注释说明返回值可能为 null。虽然比什么都不做强,但编译器并不会强制客户端进行空值检查。类似地,像 @NotNull 这样的注解也存在同样缺陷——它们无法在编译期强制执行。
Optional 类
我们需要一种机制,让方法开发者能显式声明返回值可能存在,也可能不存在。这一机制在 JDK 8(原文误写为 JDK 7,实际为 JDK 8)中以 Optional 类的形式引入。
Optional 是一个包装类,用于封装一个可能不存在的对象。因此,如果我们的 doSomething() 方法可能无法返回有效的 Foo 对象,我们可以将其签名改为:
public Optional<Foo> doSomething();
如后文所示,Optional 提供了一套方法(很多是函数式的),让客户端可以决定在值不存在时该如何处理。例如,我们可以使用 orElse 方法在值不存在时返回默认值(在 Optional 术语中称为“空 Optional”):
Foo foo = doSomething()
.orElse(new Foo());
或者使用 orElseThrow 在 Optional 为空时抛出异常:
Foo foo = doSomething()
.orElseThrow(SomeException::new);
需要注意两点:
- Java 编译器强制我们处理空
Optional的情况 - 客户端负责处理缺失值
与文档和注解不同,Optional 要求客户端必须做出决策。例如,以下代码将无法编译:
Foo foo = doSomething(); // 错误:类型不匹配
编译器会报错:Optional<Foo> 不能直接赋值给 Foo。我们必须调用 orElse、orElseThrow 或 get(但 get 不推荐作为首选)等方法,将 Optional<Foo> 转换为 Foo。而这些方法通常需要参数,从而迫使我们明确指定默认值或异常类型。
客户端责任
这引出了第二点:处理空 Optional 的责任在客户端。
通过返回 Optional<Foo> 而非 Foo,doSomething() 方法实际上是在告诉客户端:“我可能找不到结果”。因此,客户端必须处理“无结果”的情况。
这种设计意味着:方法开发者没有足够信息来决定缺失值时该做什么。虽然开发者可以抛出异常,但“值不存在”未必是异常情况。
例如,如果我们想“查找对象,若不存在则创建一个”,那么对象不存在并不是错误,抛异常反而不合适。若使用异常方式:
public Foo findIfExists() throws FooNotFoundException;
使用时需这样写:
Foo foo = null;
try {
foo = findIfExists();
} catch (FooNotFoundException e) {
foo = // 创建默认值...
}
而如果返回 Optional:
public Optional<Foo> findIfExists();
则只需:
Foo foo = findIfExists()
.orElse(/* 创建默认值... */);
后者可读性更强:仅看代码就能理解“若存在则用之,否则用这个值”。前者则需通过理解 catch 块才能明白其含义。因此,Optional 的语义更贴近意图。
总结:当“值缺失”不是错误,且客户端应负责处理时,Optional 是有效手段。若缺失值确实代表严重错误,则应抛出异常。
null 的 Optional 对象?
有一个重要注意事项:Optional 对象本身也可能为 null!
因为 Optional 本身是非基本类型,所以以下代码能编译:
public Optional<Foo> doSomething() {
return null;
}
这会导致客户端既要处理 null 返回值,又要处理空 Optional:
Optional<Foo> possibleFoo = doSomething();
if (possibleFoo == null) {
// 处理缺失...
} else {
Foo foo = possibleFoo.orElse(/* 处理缺失... */);
}
这不仅造成逻辑重复,还重新引入了空值检查的混乱。
根据 Optional 官方文档:
类型为
Optional的变量本身绝不应为null;它应始终指向一个Optional实例。
如果方法返回 null 而不是 Optional 实例,这是违反契约的行为。既然方法声明返回 Optional,就应保证永不返回 null。若无值,应返回 Optional.empty()。
因此,我们应假定 Optional 对象永远不会为 null。若实际出现 null,这是方法实现者的错误,而非客户端的责任。
核心方法
理解了 Optional 的设计理念后,我们来看其实际用法。Optional 的方法可分为两类:创建方法和实例方法。
创建方法
of
包装非空对象:
Optional<Foo> foo = Optional.of(new Foo()); // 成功
Optional<Foo> foo = Optional.of(null); // 抛出 NullPointerException
ofNullable
安全包装可能为 null 的对象:
Optional<Foo> foo1 = Optional.ofNullable(new Foo()); // 包装
Optional<Foo> foo2 = Optional.ofNullable(null); // 返回 empty()
empty
创建空 Optional:
Optional<Foo> foo = Optional.empty();
// 等价于 Optional.ofNullable(null)
实例方法
isPresent() 与 isEmpty()
isPresent():有值返回trueisEmpty()(JDK 11+):无值返回true
populated.isPresent(); // true
populated.isEmpty(); // false
empty.isPresent(); // false
empty.isEmpty(); // true
get()
- 有值时返回值
- 无值时抛出
NoSuchElementException
慎用!通常需先检查 isPresent(),但这又回到了 null 检查的老路:
if (possibleFoo.isPresent()) {
Foo foo = possibleFoo.get();
// ...
}
建议优先使用 orElse、orElseThrow 等方法。
Brian Goetz(Java 语言架构师)曾说:
“我们在 Java 8 中犯的一个错误就是命名了
Optional.get(),因为它诱使人们在不调用isPresent()的情况下直接调用它,完全违背了使用Optional的初衷。”
JDK 10 引入了 orElseThrow() 作为 get() 的语义更清晰的替代。
orElse 系列
orElse(T other):立即计算默认值orElseGet(Supplier<? extends T> supplier):惰性计算默认值(仅在需要时)
Foo foo = possibleFoo.orElse(new Foo()); // 总是创建新对象
Foo foo = possibleFoo.orElseGet(() -> expensiveDefault()); // 仅在为空时调用
orElseThrow 系列
orElseThrow():抛出NoSuchElementExceptionorElseThrow(Supplier<X> exceptionSupplier):抛出自定义异常
Foo foo = possibleFoo.orElseThrow();
Foo foo = possibleFoo.orElseThrow(SomeException::new);
ifPresent 系列
ifPresent(Consumer<T> action):有值时执行操作ifPresentOrElse(Consumer<T>, Runnable):有值执行前者,无值执行后者
possibleFoo.ifPresent(foo -> foo.doSomething());
possibleFoo.ifPresentOrElse(
foo -> foo.update(),
() -> log("No foo found")
);
map
对值进行转换(函数式管道):
Optional<Person> person = db.findPerson()
.map(mapper::fromDto);
⚠️ 注意:不要在 map 中返回 null 来表示“无值”,这违背了 Optional 的初衷。
flatMap
当转换本身可能失败时使用。flatMap 的函数应返回 Optional:
Optional<Person> person = db.findPerson()
.flatMap(dto -> {
if (canConvert(dto)) {
return Optional.ofNullable(mapper.fromDto(dto));
} else {
return Optional.empty();
}
});
filter
根据条件过滤:
Optional.of(new Bar(1))
.filter(bar -> bar.getNumber() > 0)
.isPresent(); // true
Optional.of(new Bar(-1))
.filter(bar -> bar.getNumber() > 0)
.isPresent(); // false
stream()(JDK 9+)
将 Optional 转为 Stream:
Set<Person> people = findPerson().stream()
.filter(...)
.map(...)
.collect(Collectors.toSet());
何时使用,何时不用?
✅ 推荐:作为方法返回值
官方文档明确指出:
Optional主要用于方法返回类型,当需要明确表示“无结果”且使用null极易引发错误时。
适用场景:
- 值可能存在也可能不存在
- 值缺失不是错误
- 客户端应负责处理缺失情况
典型例子:Repository 查询
public interface BookRepository {
Optional<Book> findById(long id);
Optional<Book> findByTitle(String title);
}
❌ 不推荐:作为字段(成员变量)
public class Bar {
private Optional<Foo> foo; // 不推荐!
}
原因:
Optional不可序列化- 它不是通用的“Maybe”类型,而是专为返回值设计
正确做法:字段用普通类型,getter 返回 Optional:
public class Bar {
private Foo foo;
public Optional<Foo> getFoo() {
return Optional.ofNullable(foo);
}
}
❌ 不推荐:作为方法参数
public void doSomething(Optional<Foo> foo) { ... } // 不推荐
应使用方法重载或不同方法名:
public void doSomething() { ... }
public void doSomething(Foo foo) { ... }
// 或
public void doSomething() { ... }
public void doSomethingWithFoo(Foo foo) { ... }
替代方案
1. 使用 null
仅在性能极度敏感(如底层网络/驱动代码)时考虑。绝大多数应用中,Optional 的开销微不足道。
2. 空对象模式(Null Object)
创建一个特殊子类,实现“无值”时的行为:
public class NullArticle extends Article {
@Override
public void submit() {
throw new ArticleNotFoundException();
}
}
// Repository 返回 NullArticle 而非 null
适用于方法自身知道如何处理缺失值的场景。
3. 抛出异常
当“值不存在”确实是错误时:
public Article findById(long id) {
if (found) return article;
else throw new ArticleNotFoundException();
}
结论
在应用程序中,值可能存在也可能不存在,如何优雅地处理这种情况是良好设计的关键。自 JDK 8 起,Java 提供了 Optional 类,让开发者能明确表达“可能无值”,并让客户端根据上下文决定如何处理。
关键原则:
Optional仅用于方法返回值- 绝不用于字段或参数
- 避免
get(),优先使用orElse、orElseThrow、ifPresent等 - 永远不要返回
null的Optional
掌握 Optional 的正确用法,是精通现代 Java 的重要一步。