
这篇从 thenApply、thenCompose、exceptionally、handle、whenComplete 的真实边界讲起,再补上 join/get、Async 执行器和 allOf 追问,帮你把 CompletableFuture 异常链答成可验证的工程判断。
CompletableFuture 面试最容易丢分的地方,不是你记不住 20 个 API,而是把「继续传递结果」「恢复一个默认值」「记录完成状态」混成了同一件事。| 你的目的 | 优先考虑 | 面试时要说清的边界 |
|---|---|---|
| 正常结果继续加工 | thenApply | 上一步异常完成时,依赖阶段不会凭空恢复 |
| 把一个异步阶段接到另一个异步阶段 | thenCompose | 返回的是下一个 CompletionStage,不是嵌套的 Future |
| 失败时给出替代值 | exceptionally | 只在当前阶段异常完成时执行 |
| 成功和失败都要转成新结果 | handle | 同时拿到结果和异常,二者可能有一个为空 |
| 成功和失败都要做记录、清理或打点 | whenComplete | 观察完成状态,默认保留原来的结果或异常 |
thenApply 和 thenCompose 说成一回事thenApply 适合「拿到一个结果,再同步映射成另一个结果」;thenCompose 适合「拿到一个结果,再发起另一个异步阶段」。官方文档把后者描述为把返回的另一个 CompletionStage 接平,避免出现嵌套阶段。2CompletableFuture<User> userFuture = loadUser();
// 结果是 CompletableFuture<CompletableFuture<List<Order>>>,通常不是你想要的
CompletableFuture<CompletableFuture<List<Order>>> wrong =
userFuture.thenApply(user -> loadOrders(user.id()));
// 把下一个异步阶段接平,结果是 CompletableFuture<List<Order>>
CompletableFuture<List<Order>> right =
userFuture.thenCompose(user -> loadOrders(user.id()));thenApply是对结果做映射,函数通常返回普通值;thenCompose是连接两个异步阶段,函数返回另一个 Future。只要下一步本身是异步调用,我会优先考虑thenCompose,避免把 Future 套在 Future 里面。
thenCompose 不是异常恢复器;恢复要放在合适的位置使用 exceptionally、handle 或其它异常处理阶段。2exceptionally 做日志,却没说清它会恢复结果exceptionally 只在当前阶段异常完成时执行,并且它的返回值会成为后续阶段看到的替代结果。它更像异步链里的「catch + fallback」,不是单纯的日志钩子。1CompletableFuture<List<Order>> orders = loadOrders(userId)
.exceptionally(ex -> {
log.warn("load orders failed, userId={}", userId, ex);
return List.of(); // 后续拿到的是空列表,异常已被替代
});我不会默认把所有异常都降级成空结果。对可接受的超时或依赖不可用,可以返回明确的降级对象;对数据一致性或权限类异常,则继续抛出,交给上层统一处理,并通过日志和指标区分真实空结果与降级结果。
.exceptionally(ex -> {
Throwable cause = ex instanceof CompletionException
? ex.getCause() : ex;
if (cause instanceof TimeoutException) {
return cachedOrders();
}
throw new CompletionException(cause);
});handle、whenComplete 都说成「finally」handle 会拿到 (result, exception),并计算一个新的结果,适合「无论成功失败,都转换成统一返回对象」。whenComplete 会拿到 (result, exception) 做观察动作,返回阶段默认保留原来的结果或异常,适合日志、指标、清理。null,不能只靠结果是否为空判断成功失败,应检查异常参数。2// 统一成业务结果:需要 handle
CompletableFuture<CallResult> response = callRemote()
.handle((value, ex) -> {
if (ex == null) {
return CallResult.success(value);
}
return CallResult.failed("依赖服务异常");
});
// 只做观测:需要 whenComplete
CompletableFuture<Response> observed = callRemote()
.whenComplete((value, ex) -> {
metrics.record("remote.call", ex == null ? "success" : "failure");
});whenComplete 里的记录逻辑抛异常怎么办」,不要回答「不会」。官方语义是:如果源阶段成功,而 whenComplete 的 action 自己抛异常,返回阶段会异常完成;如果源阶段本来就异常,源异常优先传播。2join() 和 get()join(),而不是 get()?关键差异至少有两个:join() 在阶段异常完成时抛出未检查的 CompletionException,底层异常在 cause 中。get() 在阶段异常完成时抛出 ExecutionException;带超时的版本还涉及超时异常,调用线程中断也需要按 checked exception 处理。try {
List<Order> orders = ordersFuture.join();
} catch (CompletionException ex) {
Throwable cause = ex.getCause();
// 根据 cause 做分类处理
}在已经处于异步流水线、并且方法签名不希望继续暴露 checked exception 时,我可能使用join(),但会在边界统一解包和分类;如果需要超时控制、响应式处理或显式处理中断,就不能只图代码短,应该选择合适的get(timeout, unit)或继续保持异步链。
join() 说成「不会阻塞」。它仍然可能等待结果;它只是异常类型和调用方式不同。Async 的执行器,导致回答停留在 API 层Async 的依赖动作,可能由完成当前阶段的线程执行,也可能由调用完成方法的线程执行。Executor 的 xxxAsync 方法,默认使用 ForkJoinPool.commonPool();如果 common pool 的并行度不足 2,文档规定会改为为每个任务创建新线程。ExecutorService ioPool = Executors.newFixedThreadPool(32);
CompletableFuture<Response> response =
CompletableFuture.supplyAsync(() -> callBlockingService(), ioPool)
.thenApplyAsync(this::toResponse, cpuPool);allOf 等完了,结果在哪里?CompletableFuture.allOf(...) 返回的是 CompletableFuture<Void>,它表达的是「所有给定阶段都完成后,这个汇总阶段才完成」,不会自动把每个 Future 的结果装进一个列表。1CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2, f3);
List<Result> results = all.thenApply(v -> List.of(
f1.join(), f2.join(), f3.join()
)).join();CompletableFuture的异常处理,我会先区分结果转换、异步阶段串联和异常恢复。thenApply处理普通结果映射,thenCompose连接下一个异步阶段;如果要给失败提供替代值,用exceptionally,如果成功失败都要转成统一业务结果,用handle,只做日志和指标则用whenComplete。最终取结果时,我会区分join的CompletionException和get的ExecutionException,并在边界解包原始异常。涉及Async方法时,再说明默认执行器和阻塞 IO 的隔离,避免只谈 API 不谈容量与故障处理。
thenApply 与 thenCompose,并写出返回类型差异?exceptionally 是替代结果,而不只是记录日志?handle,什么时候用 whenComplete?join() 的 CompletionException 与 get() 的 ExecutionException?Async 与带 Async 的执行线程差异?allOf 为什么返回 Void,以及如何读取各 Future 的结果?docs.oracle.com
docs.oracle.com
This story was produced automatically by a channel. One sentence is all it takes for Neodrop to keep producing for you.