Java Optional:正确使用方法和常见反模式
1. 概述
Optional 自 Java 8 起引入,用来更清楚地表示“可能不存在的值”,相比 null 更直观。但很多开发者滥用了它:把 Optional 当成 field、参数,或者直接调用 .get(),好像它永远不会为空。这篇文章会帮你理解何时应该使用 Optional、正确的模式,以及列举常见的反模式来避免。
读完之后,你将学会如何合理设计返回 Optional 的 API、用 map/filter/orElseGet 优雅地处理空值,以及立即识别那些“假 Optional”的代码片段并重构它们。
2. Optional 是什么以及何时使用
Optional<T> 是一个容器,可以包含或不包含 T 类型的值。主要目的是:在方法签名中明确表示返回值可能不存在,迫使调用者处理这种情况,而不是忘记检查 null。
public class CustomerRepository {
private final Map<Long, Customer> store = new HashMap<>();
public Optional<Customer> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
}
findById 返回 Optional<Customer> 而不是 Customer(可能为 null)。调用者看到这个签名就立即明白需要处理找不到的情况,不用再去查文档。
注意:
Optional是为了返回值而设计的,不是为了系统中所有可能为 null 的地方。
3. 创建 Optional 的方式
Optional<String> present = Optional.of("Hello"); // 值肯定不为 null
Optional<String> empty = Optional.empty(); // 空
Optional<String> nullable = Optional.ofNullable(null); // 可能为 null,自动转成空
Optional.of(value)如果value是null会立即抛出NullPointerException— 当你确定值不为 null 时使用。Optional.ofNullable(value)更安全,适合在值来自不可控源的情况(查询结果、map.get 等)。Optional.empty()创建一个空实例,当预先知道没有值时使用。
4. 正确处理其中的值
4.1. map、filter、flatMap
public record Order(Long id, BigDecimal amount, String couponCode) {}
public class DiscountService {
public Optional<BigDecimal> calculateDiscount(Order order) {
return Optional.ofNullable(order.couponCode())
.filter(code -> code.startsWith("SALE"))
.map(code -> order.amount().multiply(BigDecimal.valueOf(0.1)));
}
}
filter 只有在条件为真时才保留值,否则返回 Optional.empty()。map 在值存在时转换它,如果为空则什么都不做。这样的链式调用完全避免了嵌套的 if-else null 检查。
4.2. orElse、orElseGet、orElseThrow
public class PaymentService {
private final CustomerRepository repository;
public PaymentService(CustomerRepository repository) {
this.repository = repository;
}
public Customer resolveCustomer(Long id) {
return repository.findById(id)
.orElseThrow(() -> new CustomerNotFoundException(id));
}
public String resolveCustomerName(Long id) {
return repository.findById(id)
.map(Customer::name)
.orElse("Guest");
}
}
orElseThrow(supplier)在找不到时抛出有意义的异常 — 应该在值是必需的地方使用。orElse("Guest")用于默认值是简单常量的情况,无论 Optional 是否有值都会被计算。orElseGet(supplier)用于默认值计算成本较高的情况 — 只在 Optional 为空时调用 supplier。
// 错误:无论是否有值都会调用 buildDefaultCustomer()
repository.findById(id).orElse(buildDefaultCustomer());
// 正确:只在真正需要时调用
repository.findById(id).orElseGet(this::buildDefaultCustomer);
4.3. ifPresent 和 ifPresentOrElse
public class NotificationService {
public void notifyCustomer(Optional<Customer> customer) {
customer.ifPresentOrElse(
c -> System.out.println("发送邮件到 " + c.email()),
() -> System.out.println("没有客户可以发送")
);
}
}
ifPresentOrElse(自 Java 9 起)将有值和无值两种情况放在一个调用中处理,替代冗长的 if (optional.isPresent()) {...} else {...}。
5. 用 JUnit 5 + AssertJ 测试行为
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.26.3</version>
<scope>test</scope>
</dependency>
class DiscountServiceTest {
private final DiscountService service = new DiscountService();
@Test
void shouldReturnDiscountWhenCouponValid() {
Order order = new Order(1L, BigDecimal.valueOf(100), "SALE10");
Optional<BigDecimal> discount = service.calculateDiscount(order);
assertThat(discount)
.isPresent()
.get()
.isEqualTo(BigDecimal.valueOf(10.0));
}
@Test
void shouldReturnEmptyWhenNoCoupon() {
Order order = new Order(2L, BigDecimal.valueOf(100), null);
Optional<BigDecimal> discount = service.calculateDiscount(order);
assertThat(discount).isEmpty();
}
}
AssertJ 有专门为 Optional 设计的 assertion:isPresent()、isEmpty()、hasValue(...)。使用它们而不是调用 .get() 然后比较,测试代码会更自然易读。
6. 常见的反模式
6.1. 调用 .get() 而不检查
// 错误:如果 Optional 为空会抛出 NoSuchElementException,
// 完全抵消了使用 Optional 的目的
Customer customer = repository.findById(id).get();
直接调用 .get() 会使 Optional 变成被包装了一层的 null — 你仍然可能在运行时崩溃,还多浪费了一行代码。应该使用 orElseThrow()(异常更清楚)或通过 map/ifPresent 处理。
6.2. 把 Optional 用作 entity/DTO 中的 field
// 错误:Optional 不实现 Serializable,不应该出现在 JPA entity 中
@Entity
public class Customer {
private Optional<String> middleName; // 避免
}
Optional 没有实现 Serializable,不被设计成 field 或构造函数参数。如果 middleName 可以为 null,就让它保持为 String 和 null — 只在返回时从 getter 包装 Optional(如果需要的话):
// 正确:field 仍是 String,只在 getter 中根据需要包装 Optional
public Optional<String> getMiddleName() {
return Optional.ofNullable(middleName);
}
6.3. 把 Optional 用作方法参数
// 错误:强制调用者在调用前自己包装 Optional.of/ofNullable
public void updateAddress(Optional<String> newAddress) { ... }
这种方式把复杂性转移给调用者,相比重载或 @Nullable 没有任何好处。应该使用重载或传递 String 并进行常规的 null 检查:
// 正确
public void updateAddress(String newAddress) {
this.address = newAddress; // 调用者自己决定是否传 null
}
6.4. 检查 isPresent() 然后 get(),就像旧的 if-null 模式
// 错误:重新写一遍旧风格的 null 检查,浪费了 Optional 的优势
if (repository.findById(id).isPresent()) {
Customer c = repository.findById(id).get(); // 调用查询两次!
System.out.println(c.name());
}
这段代码既冗长,如果 findById 是真实查询的话还会调用两次。改用:
// 正确
repository.findById(id).ifPresent(c -> System.out.println(c.name()));
6.5. 把 Optional 用于 collection
// 错误:collection 已经有"空"的概念(empty list),不需要 Optional
public Optional<List<Order>> findOrdersByCustomer(Long customerId) { ... }
返回 Optional<List<Order>> 强制调用者处理两层空状态:Optional 为空和列表为空 — 这是多余的。当没有数据时应该总是返回空 List:
// 正确
public List<Order> findOrdersByCustomer(Long customerId) {
return orders.getOrDefault(customerId, List.of());
}
6.6. 在高性能 hot path 中使用 Optional
Optional 会创建额外的对象包装器,对大多数应用来说不是大问题,但在每秒运行数百万次的代码中(比如处理大数据流的循环),持续的装箱可能会影响垃圾回收。在这些情况下,考虑直接检查 null 或使用 sentinel value。
注意: 不要过早优化 — 只在你已经 profile 过并发现
Optional真的是瓶颈时才考虑这个。
7. 何时该/不该使用 Optional 的速查表
| 情况 | 应该使用 Optional? |
|---|---|
| Method 的返回值,可能不存在 | ✅ 应该 |
| entity/DTO 中的 field | ❌ 不应该 |
| 构造函数/方法的参数 | ❌ 不应该 |
| 返回 collection(List、Set、Map) | ❌ 不应该 — 返回空 collection |
| 方法中的局部变量以避免重复 null 检查 | ⚠️ 看情况 — 考虑一下,有时 if/else 更清楚 |
8. 总结
Optional 是一个很好的工具,可以使 API 契约更清晰,避免在调用层面出现 NullPointerException,但只有在正确的地方使用才能发挥价值:method 的返回值。避免把它变成 field、参数或 collection 的包装器 — 在这些地方 Optional 只会让代码更混乱而解决不了任何问题。编写新代码时,问自己:“这里的 Optional 是否让调用者更清楚地处理情况,还是只是把 null 包装了一层?” — 答案会告诉你是否应该保留或移除它。