CompletableFuture API 过于复杂?选取7个最常用的方法,解决95%的问题

前言

截止 JDK25,CompletableFuture 已经支持68个实例方法,12个静态方法。这些方法虽然方便了使用者,但对于初学者来说无疑是过于复杂和难以记忆的。本文中,我将秉持极简和实用的原则,选取最核心、最常用的API方法,同时分析为什么其他方法没有被选择进来。

注意,本文的观点可能相当激进,可能挑战固有认知,希望大家聚焦真正能解决问题的核心API。

如果你使用的JDK1.8版本,可以使用类库CFFU2,其提供了 CompletableFuture 在新版JDK中的所有方法支持(backport)。CFFU2 不仅提供了新版本JDK的API回溯支持,更以其增强的编排能力,弥补了原生API的一些不足,完美契合了本文的观点。使用方法:所有的JDK9+方法都可以通过 CompletableFutureUtils 中对应名称的静态方法实现。

xml 复制代码
<dependency>
  <groupId>io.foldright</groupId>
  <artifactId>cffu2</artifactId>
  <version>2.0.1</version>
</dependency>

核心API精选

这里选择了7个最常用的方法,组合起来使用,基本可以解决大多数异步编程问题。所有带 Async 后缀的方法,都推荐显式传入 Executor 参数,以确保任务在预期的线程池中执行,避免资源争用和不可预测的性能问题。

1. 创建(封装固定值或者异常)(2个)

  • static <U> CompletableFuture<U> completedFuture(U value) : 返回一个已经以给定值正常完成的 CompletableFuture。用于将一个已知结果封装为 CompletableFuture
  • static <U> CompletableFuture<U> failedFuture(Throwable ex) (Since 9) : 返回一个已经以给定异常完成的 CompletableFuture。用于将一个已知异常封装为 CompletableFuture

2. 数据转换与异常恢复(4个)

  • thenApplyAsync(Function<? super T, ? extends U> fn, Executor executor) : 当前阶段正常完成时,将结果作为输入,通过 fn 转换并返回一个新的 CompletableFuture。类似于 Optional.map(推荐显式指定执行器)
  • thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn, Executor executor) : 当前阶段正常完成时,将结果作为输入,通过 fn 返回一个新的 CompletionStage。用于扁平化嵌套的异步操作,类似于 Optional.flatMap(推荐显式指定执行器)
  • exceptionallyAsync(Function<Throwable, ? extends T> fn, Executor executor) : 当当前阶段异常完成时,执行 fn 来恢复,并返回一个正常完成的新 CompletableFuture(推荐显式指定执行器)
  • exceptionallyComposeAsync(Function<Throwable, ? extends CompletionStage<T>> fn, Executor executor) (Since 12) : 类似于 exceptionallyAsync,但 fn 返回一个 CompletionStage,用于更复杂的异常恢复逻辑,例如在异常时启动另一个异步任务。 (推荐显式指定执行器)

3. 超时控制(1个)

  • CompletableFuture<T> orTimeout(long timeout, TimeUnit unit) : 如果在指定时间内未完成,则此 CompletableFuture 会以 TimeoutException 异常完成。这是实现健壮异步服务不可或缺的方法。

获取结果(阻塞与非阻塞)

  • T join() : 阻塞等待任务完成,并返回结果。如果异常完成,则抛出非受检的 CompletionException 。通常比 get() 更适合链式调用,因为它避免了繁琐的受检异常处理。
  • T getNow(T valueIfAbsent) : 非阻塞。如果已完成则返回结果,否则返回 valueIfAbsent。如果异常完成,则抛出 CompletionException。适用于需要立即获得结果或提供默认值的场景。
  • T resultNow() (Since 19) : 非阻塞。如果已成功完成,直接返回结果。这是对 CompletableFuture 状态进行"模式匹配"的更优雅方式。
  • Throwable exceptionNow() (Since 19) : 非阻塞。如果已异常完成,直接返回异常。与 resultNow() 配合,可以清晰地判断并处理最终结果或异常。

任务编排(强烈推荐CFFU2)

原生API自带的编排方法(如 allOfanyOf)能力有限且并不常用,尤其在错误处理方面存在不足。这里我们强烈推荐使用CFFU2提供的编排方法,它们提供了更强大、更符合实际生产需求的编排能力。

swift 复制代码
// CfIterableUtils
// 聚合所有任务的结果,任何一个任务失败,整个编排立即失败(fail-fast)
public static <T> CompletableFuture<List<T>> allResultsFailFastOf(Iterable<? extends CompletionStage<? extends T>> cfs)

// CfTupleUtils, 支持多参数,聚合不同类型的任务结果,任何一个任务失败,整个编排立即失败(fail-fast)
public static <T1, T2> CompletableFuture<Tuple2<T1, T2>> allTupleFailFastOf(
    CompletionStage<? extends T1> cf1, CompletionStage<? extends T2> cf2)
// ... 还有allTupleFailFastOf(cf1, cf2, cf3) 等更多重载

failFast 的含义是:

当任何一个子任务失败时,整个编排任务(例如 allResultsFailFastOfallTupleFailFastOf)会立即以该失败告终 ,而不是像原生 allOf 那样等待所有任务都完成后才抛出异常。这种行为在很多实际场景中更为合理和高效,可以避免不必要的计算和资源消耗。


那些用处不大的方法

以下方法,在笔者实践和理解中,要么可以通过之前推荐的方法组合实现,要么使用场景过于特殊,要么存在设计缺陷,不应成为初学者学习的重点,应该谨慎使用,有些甚至应该尽量避免使用。

  1. 就地执行、使用默认执行器的异步执行相关方法:

    • 不带 Async 后缀的方法 :如 thenApply(Function)。它们在当前线程同步执行,可能阻塞调用者,导致性能问题或死锁。
    • Async 后缀但未传入 Executor 参数的方法 :如 thenApplyAsync(Function)。它们使用 ForkJoinPool.commonPool() 作为默认执行器,可能导致任务队列饱和、线程耗尽,影响整个应用的性能。
    • 替代方案 :所有带Async后缀的方法,都推荐显式传入 Executor 参数 。如果确实需要就地执行,可以通过传入 MoreExecutors.directExecutor() (Guava) 或 Runnable::run (Java 9+) 来实现,但应清楚其潜在风险。
  2. RunnableSupplierConsumer 相关方法:

    • thenRunthenAcceptsupplyAsync。这些方法虽然提供了便利,但从函数式编程的角度看,它们都可以通过 Function 的变体(输入或输出为 Void)来表达。例如,thenRun 等同于 thenApply(v -> null)thenAccept 等同于 thenApply(t -> { consumer.accept(t); return null; })
    • 替代方案 :统一使用 thenApplyAsyncthenComposeAsync 有助于简化概念模型,减少需要记忆的方法数量,并鼓励更纯粹的函数式编程风格。
  3. 两个元素关系的相关方法:oreitherboth

    • acceptEitherrunAfterBoth。这些方法命名复杂且难以记忆,只适用于两个 CompletableFuture 的简单组合。
    • 替代方案:可以通过 CFFU2 提供的支持元组、列表的编排方法实现更灵活、更可扩展的组合逻辑。
  4. 同时处理值和异常的相关方法:handlewhenComplete

    • 这两个方法的名字就挺奇怪且难记。handle 相当于 thenApply 结合 exceptionally 的组合,而 whenComplete 则是 thenAccept 结合 exceptionally
    • 替代方案 :通过组合前文推荐的 thenApplyAsync / thenComposeAsyncexceptionallyAsync / exceptionallyComposeAsync,可以更清晰地分离正常流程与异常恢复逻辑,提高代码可读性。
  5. anyOfallOf (原生API):

    • anyOf:个人经验中用的不多,虽然看上去很有用,但实际场景中通常需要更复杂的逻辑来处理多个成功结果或错误。
    • allOf:原生 allOf 方法是非 fail-fast 的,即一个任务失败后,它会等待所有任务完成才抛出异常,这在需要快速响应失败的场景下是低效的。
    • 替代方案 :对于 anyOf,通常可以用 orTimeoutexceptionally 组合实现更精细的控制。对于 allOf强烈推荐使用 CFFU2 提供的 allResultsFailFastOfallTupleFailFastOf,它们提供了更健壮、更符合实际需求的 fail-fast 编排能力。
  6. 一些获取结果相关方法:isDoneisCanceledisCompletedExceptionally

    • 这些方法提供了任务状态的布尔判断。
    • 替代方案 :由于新版 CompletableFuture 提供了 resultNow()exceptionNow() 这种"模式匹配"类似的方法,结合 try-catchOptional 包装,可以更直接、更清晰地获取结果或异常,而无需先判断状态。get() 方法由于处理受检异常过于麻烦,更推荐使用 join()resultNow() / exceptionNow() 组合处理结果。
  7. 提供给子类实现的相关方法:newIncompletableFuturedefaultExecutortoCompletableFuture 等:

    • 这些方法主要用于 CompletableFuture 的内部扩展或与其他 CompletionStage 实现的互操作,对于日常使用者来说,真正使用到的机会很少。
  8. 一些"差强人意"的实现:cancel

    • 原生 cancel 方法只设置任务状态为 CANCELLED,并不会中断正在执行的底层任务,也无法向下游传播取消信号,其行为常常不符合预期,导致取消逻辑复杂且不可靠。
    • 替代方案 :更推荐通过 orTimeout 等机制进行超时控制,或者在任务内部实现协作式取消逻辑。
  9. 为了实现"只读"或者说防止篡改的实现:completedStagefailedStageminimalCompletionStagecopy 等:

    • 这些方法的核心思想是防御性编程,旨在返回一个不可修改的 CompletionStage。当你需要严格的防御性编程时用处很大,但考虑到实际开发效率,每次都写这种代码更像是在防御自己,而不是解决业务问题。
    • 替代方案 :在大多数业务场景中,直接返回 CompletableFuture 即可,通过良好的代码规范和团队协作来避免误用。
  10. 支持竞争写、篡改的实现:completecompleteExceptionallyobtrudeValueobtrudeException

    • 这些方法允许外部强制设置 CompletableFuture 的结果或异常,甚至覆盖已有的结果。它们打破了 CompletableFuture 通常的单次完成语义。
    • 建议 :应该尽量避免使用 这些方法,除非你非常清楚其副作用,且用于将非 CompletableFuture 的异步源桥接到 CompletableFuture 生态系统。滥用会导致难以追踪的并发问题。
  11. 监控、调试相关方法:getNumberOfDependents

    • 这类方法提供了 CompletableFuture 内部状态的快照,但通常不用于业务逻辑。
    • 建议:更推荐使用专业的监控工具或日志系统来调试和观察异步任务。
  12. 延迟执行相关方法:delayedExecutor

    • delayedExecutor 使用到了 CompletableFuture 内部维护的 delayer 线程。
    • 建议 :建议理解任务调度器 ScheduledThreadPoolExecutor 的核心思想后,全局使用自己维护的唯一全局任务调度器来管理延迟任务,以便更好地控制资源和生命周期。

通过这套精简的API,你将能够更高效、更清晰地编写 CompletableFuture 相关的异步代码,避免陷入API的海洋而不知所措。正所谓少即是多,聚焦核心才能真正掌握异步编程的精髓。

相关推荐
冲鸭ONE3 小时前
新手搭建Spring Boot项目
spring boot·后端·程序员
Moonbit3 小时前
MoonBit Pearls Vol.10:prettyprinter:使用函数组合解决结构化数据打印问题
前端·后端·程序员
数智顾问3 小时前
Flink ProcessFunction 与低层级 Join 实战手册:多流广告计费精确去重
java·spring boot·spring
世界哪有真情4 小时前
Trae 蓝屏问题
前端·后端·trae
一头生产的驴4 小时前
java整合itext pdf实现固定模版pdf导出
java·python·pdf
魔都吴所谓4 小时前
【python】快速实现pdf批量去除指定位置水印
java·python·pdf
Camel卡蒙4 小时前
数据结构——字典树Trie(介绍、Java实现)
java·数据结构
Moment4 小时前
NestJS 在 2025 年:对于后端开发者仍然值得吗 😕😕😕
前端·后端·github
kida_yuan4 小时前
【Java】基于 Tabula 的 PDF 合并单元格内容提取
java·数据分析