《Java 编程入门》12.3 Optional 与惰性求值

从 findById 返回 Optional 却总被 get() 的写法出发,讲清 Optional 的创建、map、flatMap、filter 与三种兜底方式,用计数器证明 orElse 会提前求值而 orElseGet 不会;再把 findBook、borrowerOf 和 CLI 查找落到 library,并演示 Stream.iterate 与 anyMatch 的短路。

本节目标:能用 Optional 表达「可能找不到」,会用 map、flatMap、filter 和 orElseGet 写链式查找,能说清 orElse 与 orElseGet 的求值差异,并理解 Stream 的短路如何避免无谓计算。
适用版本:JDK 25 LTS(Temurin 25.0.5);Optional.isEmpty、ifPresentOrElse、Optional.stream 在 JDK 21 上同样可用。

上一节留下的问题

Stream 的 findFirst() 返回 Optional,仓储的 findById 也返回 Optional。但 Optional 本身不解决任何问题,它只是把「可能没有值」写进了类型。如果调用方仍然这样写:

Optional<Book> found = service.findBook("978-7-000-00000-0");
Book book = found.get();

那么「没有值」的检查只是从 null 换了个地方,运行时照样炸。另一种常见写法是先判空再取值:

if (found.isPresent()) {
    System.out.println(found.get().title());
}

这能工作,但又回到了命令式的 if,Optional 的链式能力完全没用上。本节要解决的就是:怎么把「有值就转换、没值就兜底」写成一条表达式,以及兜底逻辑该不该提前执行。

Optional 是什么,不是什么

Optional<T> 是一个最多装一个值的容器:要么装着 T,要么是空的。它的主要用途是方法返回值,用来明确告诉调用方「这次查找可能没有结果」。

它不适合做这些事:

  • 字段:类字段用可空引用或默认值,别用 Optional 包一层;Optional 不可序列化,很多框架也不喜欢它。
  • 方法参数:参数用重载或可空类型表达,Optional 参数会强迫调用方包装。
  • 集合元素:不要写 List<Optional<Book>>,直接过滤掉不存在的元素,用空列表表示「没有」。
  • 基本类型:用 OptionalInt、OptionalLong、OptionalDouble,避免装箱。

一句话:Optional 是返回类型的工具,不是「所有可能为 null 的地方都换掉」的万能替换。

创建与判断

三种创建方式:

Optional<Book> present = Optional.of(book);          // 不能为 null
Optional<Book> nullable = Optional.ofNullable(maybeNull); // 可以为 null
Optional<Book> empty = Optional.empty();             // 明确为空

Optional.of(null) 会立刻抛 NullPointerException:

Exception in thread "main" java.lang.NullPointerException
	at java.base/java.util.Objects.requireNonNull(Objects.java:220)
	at java.base/java.util.Optional.of(Optional.java:113)

ofNullable 则把 null 转成 Optional.empty()。判断有没有值,用 isPresent() / isEmpty();如果只是想在存在时做一件事,用 ifPresent:

service.findBook(isbn).ifPresent(book -> System.out.println(book.title()));

需要同时处理「有」和「没有」两个分支时,用 ifPresentOrElse:

service.findBook(isbn).ifPresentOrElse(
        book -> System.out.println("present = " + book.title()),
        () -> System.out.println("present = none"));

但大多数时候,你不需要显式判断,而是继续链式转换。

链式处理:map、flatMap、filter

map 在有值时转换,空时原样返回空:

Optional<String> title = service.findBook(isbn).map(Book::title);

flatMap 用于「转换函数本身返回 Optional」的情况。比如先找在借记录,再根据记录里的会员号找会员:

public Optional<String> borrowerOf(String isbn) {
    return loans.findActiveByIsbn(isbn)
            .flatMap(loan -> members.findById(loan.memberId()))
            .map(Member::name);
}

如果这里用 map,会得到 Optional<Optional<Member>>;flatMap 把两层压成一层。判断标准很简单:转换函数返回 Optional,就用 flatMap;返回普通值,就用 map。

filter 在值不满足条件时把它变成空:

Optional<Book> recent = service.findBook(isbn)
        .filter(book -> book.year() > 2015);

这三步可以任意组合。比如「找一本书,取标题,标题长度超过 10 才保留」:

Optional<String> longTitle = service.findBook(isbn)
        .map(Book::title)
        .filter(title -> title.length() > 10);

整条链在空值处自动短路:findBook 返回空,后面的 map 和 filter 都不会执行。

兜底:orElse、orElseGet、orElseThrow

有值就取,没值就兜底,有三种常用写法:

String a = optional.orElse("默认标题");
String b = optional.orElseGet(() -> loadDefaultTitle());
String c = optional.orElseThrow(() -> new BookNotFoundException(isbn));

三者的区别不只在返回值,还在兜底逻辑什么时候执行:

  • orElse(value) 的参数是普通值,无论 Optional 是否为空,参数表达式都会先求值。
  • orElseGet(Supplier) 的参数是 Supplier,只有 Optional 为空时才调用。
  • orElseThrow(Supplier) 只在为空时抛异常,适合「找不到就是错误」的场景。

用计数器可以直观看到差异:

AtomicInteger calls = new AtomicInteger();
String eager = Optional.of("value").orElse(compute("orElse(present)", calls));
System.out.println("eager = " + eager + ", calls = " + calls.get());

Optional.of("value").orElseGet(() -> compute("orElseGet(present)", calls));
System.out.println("lazy present, calls = " + calls.get());

String fallback = Optional.<String>empty()
        .orElseGet(() -> compute("orElseGet(empty)", calls));
System.out.println("fallback = " + fallback + ", calls = " + calls.get());

本机 JDK 25 输出:

  compute orElse(present)
eager = value, calls = 1
lazy present, calls = 1
  compute orElseGet(empty)
fallback = fallback-orElseGet(empty), calls = 2

第一行 orElse 在 Optional 有值时仍然执行了 compute,白白算了一次;orElseGet 在有值时一次都没调用,只有为空时才执行。如果兜底逻辑有代价(查数据库、读文件、创建大对象),用 orElseGet。

orElseThrow 适合把「缺失」转成领域异常:

Book book = service.findBook(isbn)
        .orElseThrow(() -> new BookNotFoundException(isbn));

第 13 章会把 BookNotFoundException 接入统一的异常体系。

Stream 的惰性与短路

12.2 说过中间操作是惰性的。惰性还有一个更实用的表现:短路。下面这条管道从一个无限流里找第一个 7 的倍数:

long first = Stream.iterate(1, n -> n + 1)
        .peek(n -> System.out.println("  visit " + n))
        .filter(n -> n % 7 == 0)
        .findFirst()
        .orElse(-1);
System.out.println("first multiple of 7 = " + first);

本机 JDK 25 输出:

  visit 1
  visit 2
  visit 3
  visit 4
  visit 5
  visit 6
  visit 7
first multiple of 7 = 7

Stream.iterate 没有上限,但 findFirst 在拿到第一个结果后就停止,所以只访问了 1 到 7。如果换成 count(),这个程序永远不会结束——count 需要处理所有元素。

anyMatch 同样短路。检查有没有 2019 年以后出版的书:

boolean modern = DemoData.BOOKS.stream()
        .peek(b -> System.out.println("  check " + b.title()))
        .anyMatch(b -> b.year() > 2018);
System.out.println("anyMatch = " + modern);

输出:

  check Effective Java
  check Java Puzzlers
  check Java 并发编程实战
  check 深入理解 Java 虚拟机
anyMatch = true

示例数据里前四本都没满足,直到第四本《深入理解 Java 虚拟机》(2019)才返回 true,后面的两本没有被检查。这不是优化技巧,而是 Stream 的语义:终止操作只需要多少元素,就只计算多少元素。

Optional.stream() 可以把「可能为空的值」融进 Stream 管道,空的 Optional 不贡献元素:

List<String> titles = Stream.of(Optional.of("A"), Optional.<String>empty(),
                Optional.of("B"))
        .flatMap(Optional::stream)
        .toList();
System.out.println(titles);

输出 [A, B]。处理「一批 id 里能查到的那些」时,这个写法比 filter(Optional::isPresent).map(Optional::get) 干净。

在 library 里落地

LibraryService 的查询方法直接返回 Optional,把「可能没有」交给调用方:

public Optional<Book> findBook(String isbn) {
    return books.findById(isbn);
}

public Optional<String> borrowerOf(String isbn) {
    return loans.findActiveByIsbn(isbn)
            .flatMap(loan -> members.findById(loan.memberId()))
            .map(Member::name);
}

findBook 只是转发仓储;borrowerOf 展示了 flatMap 的价值:在借记录和会员是两次可能失败的查找,用 flatMap 串起来,任何一步为空,整条链就是空,不需要嵌套 if。

CLI 层不应该把 Optional 继续往上传,而应该在这里转成用户能看懂的文本:

String line = service.findBook(isbn)
        .map(book -> book.title() + " / " + book.author())
        .orElse("未找到 ISBN 为 " + isbn + " 的图书");
System.out.println(line);

需要报错而不是兜底时,用 orElseThrow:

Book book = service.findBook(isbn)
        .orElseThrow(() -> new BookNotFoundException(isbn));

跑一遍完整例子:

LibraryService service = DemoData.service();
String isbn = "978-7-111-54742-6";

System.out.println("found   = " + service.findBook(isbn).map(Book::title));
System.out.println("missing = " + service.findBook("978-7-000-00000-0").map(Book::title));
System.out.println("upper   = " + service.findBook(isbn)
        .map(b -> b.title().toUpperCase()));

service.borrow(isbn, "M001");
System.out.println("borrower = " + service.borrowerOf(isbn));

try {
    service.findBook("978-7-000-00000-0")
            .map(Book::title)
            .orElseThrow(() -> new BookNotFoundException("978-7-000-00000-0"));
} catch (BookNotFoundException e) {
    System.out.println("caught  = " + e.getMessage());
}

本机 JDK 25 输出:

found   = Optional[Effective Java]
missing = Optional.empty
upper   = Optional[EFFECTIVE JAVA]
borrower = Optional[张三]
caught  = 找不到 ISBN 为 978-7-000-00000-0 的图书

注意 found 和 missing 打印的是 Optional[...] 和 Optional.empty,这是 Optional.toString() 的调试格式,不适合直接展示给用户。真正面向用户的输出要走 map(...).orElse(...) 或 ifPresentOrElse。

Optional 和 11.1 的 Result<T> 分工不同:

类型表达什么典型场景
Optional<T>有值 / 无值按 id 查找、findFirst
Result<T>成功值 / 失败消息注册图书、校验输入

Optional 不携带失败原因,只有「空」;Result 能带一条消息。两者都不应该被滥用成控制流的唯一手段,第 13 章会讲什么时候该用异常。

常见错误与边界

错误一:不检查就 get()

Optional<String> value = Optional.empty();
System.out.println(value.get());
Exception in thread "main" java.util.NoSuchElementException: No value present
	at java.base/java.util.Optional.get(Optional.java:143)

get() 是 Optional 里最不该直接用的方法。它把「可能为空」又变回了运行时异常。用 orElse、orElseGet、orElseThrow 或 ifPresent 替代。

错误二:用 isPresent + get 代替链式调用

if (service.findBook(isbn).isPresent()) {
    Book book = service.findBook(isbn).get();
    System.out.println(book.title());
}

这里查了两次,而且把 Optional 当成了 null 的包装。更好的写法是:

service.findBook(isbn).ifPresent(book -> System.out.println(book.title()));

或者需要返回值时:

String title = service.findBook(isbn).map(Book::title).orElse("未知");

错误三:在 orElse 里放昂贵操作

orElse(loadFromDatabase()) 无论 Optional 是否为空都会执行 loadFromDatabase()。这类 bug 不会报错,只会在监控里表现为「明明查到了,为什么还多了一次查询」。有副作用或开销的兜底一律用 orElseGet。

边界:Optional 不是 Serializable

Optional 没有实现 Serializable。把它放进需要序列化的 DTO、缓存值或 RPC 参数里,会在运行时失败。DTO 字段用可空引用,方法返回值才用 Optional。

小结

  • Optional<T> 表示「有值或无值」,主要用作方法返回值,不适合字段、参数和集合元素。
  • of 不接受 null,ofNullable 接受,empty 表示明确为空;ifPresentOrElse 同时处理两个分支。
  • map 转换普通值,flatMap 转换返回 Optional 的查找,filter 在不满足条件时清空。
  • orElse 的参数总会求值,orElseGet 只在为空时调用,orElseThrow 把缺失转成异常。
  • Stream 的惰性配合 findFirst、anyMatch 可以短路无限流或长集合,只计算必要的元素。
  • Optional 不携带失败原因;需要错误消息时用 Result<T>,需要中断流程时用异常。

到这里,正常路径和「找不到」都处理完了。但还有些失败不是「没有值」,而是「规则不允许」:书已被借走、会员达到借阅上限、命令格式不对。下一章建立 LibraryException 体系,把这些情况从返回值里分出来。

阅读导航:上一节:12.2 Stream 管道 · 下一节:13.1 受检与非受检异常 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Netty 高性能网络编程》15.3 linkd-v2 验收与复盘
  2. 《Netty 高性能网络编程》15.2 限流、超时与降级
  3. 《Netty 高性能网络编程》15.1 网关路由与转发