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 包装了一层?” — 答案会告诉你是否应该保留或移除它。

javaoptionalbest-practicesclean-code