1. 概述

本教程将深入探讨Monads的概念,以及如何利用它们处理副作用(side effects)。我们将重点介绍两个核心方法:map()flatMap(),它们是链式操作Monads的关键。通过分析Java生态中几种主流Monads的API,我们将聚焦其实际应用场景。

2. 副作用处理

在函数式编程中,"副作用"通常指超出函数或组件作用域的操作。为了在保持函数纯性的同时处理这些副作用,我们可以将操作或数据包装进容器中。Monads本质上就是这类容器,能将副作用隔离在函数作用域之外

举个简单例子,考虑一个整数除法函数:

double divide(int dividend, int divisor) {
    return dividend / divisor;
}

看似纯函数,但当divisor为0时会抛出ArithmeticException产生副作用。我们可以用Monads封装结果来规避:

Optional<Double> divide(int dividend, int divisor) {
    if (divisor == 0) {
        return Optional.empty();
    }
    return Optional.of(dividend / divisor);
}

现在除零操作不再产生副作用。以下是Java中处理不同副作用的常见Monads:

  • Optional<> – 处理空值问题
  • List<>, Stream<> – 管理数据集合
  • Mono<>, CompletableFuture<> – 处理并发和I/O
  • Try<>, Result<> – 处理错误
  • Either<> – 处理二元状态

3. 函子(Functors)

创建Monads时,需要允许其修改封装对象或操作,同时保持容器类型不变。以Java Stream为例:

若现实世界中Long可通过Instant.ofEpochSeconds()转为Instant,在Stream世界中这种关系也应保持。**Stream API通过高阶函数"提升"这种关系,这就是函子概念,其转换方法通常命名为map()**:

Stream<Long> longs = Stream.of(1712000000L, 1713000000L, 1714000000L);
Stream<Instant> instants = longs.map(Instant::ofEpochSecond);

虽然map是典型命名,但具体方法名不影响函子本质。例如CompletableFuture使用thenApply()

CompletableFuture<Long> timestamp = CompletableFuture.completedFuture(1713000000L);
CompletableFuture<Instant> instant = timestamp.thenApply(Instant::ofEpochSecond);

两种容器都暴露了方法,允许对封装数据应用其支持的所有操作:

monad functor

4. 绑定(Binding)

绑定是Monads的核心特性,允许在Monadic上下文中链式组合多个计算。简单说,用绑定替代map()可避免嵌套结构。

4.1. 嵌套Monads问题

若仅依赖函子序列化操作,最终会产生嵌套容器。我们用Project Reactor的Mono演示

假设有两个响应式方法获取AuthorBook

Mono<Author> findAuthorByName(String name) { /* ... */ }
Mono<Book> findLatestBookByAuthorId(Long authorId) { /* ... */ }

从作者名开始获取详情:

void findLatestBookOfAuthor(String authorName) {
    Mono<Author> author = findAuthorByName(authorName);
    // ...
}

若用map()将容器内容从Author转为Book

Mono<Mono<Book>> book = author.map(it -> findLatestBookByAuthorId(it.authorId());

结果产生嵌套的Mono容器——因为findLatestBookByAuthorId()返回Mono,而map()又包装了一层。

4.2. flatMap()解决方案

使用绑定(通常叫flatMap)可消除额外容器,扁平化结构

void findLatestBookOfAuthor(String authorName) {
    Mono<Author> author = findAuthorByName(authorName);
    Mono<Book> book = author.flatMap(it -> findLatestBookByAuthorId(it.authorId()));
    // ...
}

进一步优化,内联操作并添加中间map()提取authorId

void findLatestBookOfAuthor(String authorName) {
    Mono<Book> book = findAuthorByName(authorName)
      .map(Author::authorId)
      .flatMap(this::findLatestBookByAuthorId));
    // ...
}

组合map()flatMap()是操作Monads的高效方式,能以声明式定义转换链

5. 实战场景

Monads通过抽象层帮我们处理副作用。多数情况下,它们让我们聚焦主流程,将异常处理隔离在主逻辑之外

5.1. "铁路轨道"模式

Monads绑定也叫"铁路轨道"模式。主流程像直行轨道,异常情况则切换到平行轨道

以验证Book对象为例:检查ISBN → 验证authorId → 校验书籍类型:

void validateBook(Book book) {
    if (!validIsbn(book.getIsbn())) {
        throw new IllegalArgumentException("Invalid ISBN");
    }
    Author author = authorRepository.findById(book.getAuthorId());
    if (author == null) {
        throw new AuthorNotFoundException("Author not found");
    }
    if (!author.genres().contains(book.genre())) {
        throw new IllegalArgumentException("Author does not write in this genre");
    }
}

用Vavr的Try monad实现铁路轨道模式链式验证

void validateBook(Book bookToValidate) {
    Try.ofSupplier(() -> bookToValidate)
      .andThen(book -> validateIsbn(book.getIsbn()))
      .map(book -> fetchAuthor(book.getAuthorId()))
      .andThen(author -> validateBookGenre(bookToValidate.genre(), author))
      .get();
}

void validateIsbn(String isbn) { /* ... */ }

Author fetchAuthor(Long authorId) { /* ... */ }

void validateBookGenre(String genre, Author author) { /* ... */ }

API暴露andThen()等无需响应的方法,用于检测失败并切换轨道。而map()/flatMap()则推进流程,创建新Try<>包装响应(如Author对象):

try rail road

5.2. 故障恢复

某些API允许从失败轨道恢复到主轨道,通常需要提供回退值。例如Try<>recover()方法:

try rail road recover

5.3. 其他案例

理解Monads工作原理后,具体方法名并不重要,关键是理解其目的。Monads API方法通常分为两类:

  • 转换底层数据
  • 必要时切换轨道

例如:

  • Optionalmap()/flatMap()转换数据,用filter()/or()切换"空/存在"状态
  • CompletableFuturethenApply()/thenCombine()替代map()/flatMap(),通过exceptionally()恢复失败轨道

6. 总结

本文探讨了Monads的核心特性,通过实例演示了如何处理集合、并发、空值、异常等副作用。我们学习了"铁路轨道"模式绑定Monads,将副作用隔离在组件作用域之外。

完整源码请参考GitHub仓库


原始标题:Monads in Java | Baeldung