1. 问题:异步代码中的异常
在普通(同步)代码中很简单:如果方法里发生异常,它会沿着调用栈“向上冒泡”,我们可以用 try-catch 来捕获。例如:
try {
int x = 1 / 0;
} catch (ArithmeticException ex) {
System.out.println("除以零!");
}
在异步代码中情况更复杂。通过 CompletableFuture.supplyAsync 启动的任务在其他线程中执行。如果那里抛出异常,它不会直接抛到主线程!而是被“封装”到 CompletableFuture 对象内部,之后当你调用 get() 或 join() 时,会以 ExecutionException 的形式拿到这个异常。
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
// 哎呀,这里出错了!
return 1 / 0;
});
try {
Integer result = future.get(); // 这里会抛出异常!
} catch (Exception ex) {
System.out.println("发生错误:" + ex.getMessage());
}
但如果你不调用 get()(顺带一提,这本身就不太“异步”),而是通过 thenApply 等方法来构建链路,错误可能会“悄然丢失”。因此在异步编程中,关键是学会在 CompletableFuture 的链条里捕获并处理错误。
2. exceptionally 方法:处理错误并返回值
exceptionally 方法允许在链中前面的阶段出现异常时将其捕获、处理,并返回一个替代值。这就像异步数据流里的 catch。
签名:
CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)
使用示例
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("正在执行危险计算...");
if (Math.random() > 0.5) {
throw new RuntimeException("出了点问题!");
}
return 42;
});
future = future.exceptionally(ex -> {
System.out.println("发生错误:" + ex.getMessage());
return 0; // 返回 "安全" 的值
});
配合 thenAccept 的示例
future.thenAccept(result -> System.out.println("结果: " + result));
示例输出(可能之一):
正在执行危险计算...
发生错误:出了点问题!
结果:0
正在执行危险计算...
结果:42
重要! 只有当它之前的链路中出现了未处理的异常时,exceptionally 才会触发;如果一切正常,它会“原样传递”结果。
3. handle 方法:同时处理结果与错误的通用处理器
有时我们需要同时处理结果和错误。比如,成功则返回结果,失败则返回备选值或记录日志。
签名:
CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)
- 第一个参数——结果(如果出错则为 null),
- 第二个参数——异常(如果成功则为 null)。
使用示例
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) throw new RuntimeException("随机错误!");
return 100;
});
CompletableFuture<Integer> safeFuture = future.handle((result, ex) -> {
if (ex != null) {
System.out.println("发现错误:" + ex.getMessage());
return -1;
}
return result;
});
safeFuture.thenAccept(r -> System.out.println("最终结果:" + r));
输出:
发现错误:随机错误!
最终结果:-1
最终结果:100
当你希望无论任务是成功还是失败都要采取行动时,就应使用 handle。它是一个通用的收尾处理器,总会被调用,并接收两个参数:结果(成功时)和异常(出错时)。
该方法非常适合集中记录错误、在不中断链路的情况下返回默认值,或让异步流程体面地收尾。
示例:
CompletableFuture<Integer> future = CompletableFuture
.supplyAsync(() -> 10 / 0) // 这里会发生错误
.handle((result, ex) -> {
if (ex != null) {
System.out.println("错误:" + ex.getMessage());
return 0; // 默认值
}
return result;
});
System.out.println(future.join()); // 将输出 0
与 exceptionally 只在出错时触发不同,handle 总会触发,让你在同一个地方处理两种结果,并保持链路的顺滑。
4. whenComplete 方法:任务完成后的副作用动作
有时我们不需要改变结果,只是想在任务完成后执行某个动作——例如记录日志,无论成功与否。
签名:
CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)
- 第一个参数——结果(出错时为 null),
- 第二个参数——异常(成功时为 null)。
使用示例
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) throw new RuntimeException("错误!");
return 10;
});
future.whenComplete((result, ex) -> {
if (ex != null) {
System.out.println("执行时发生错误:" + ex.getMessage());
} else {
System.out.println("成功完成,结果:" + result);
}
});
重要区别:
whenComplete 不会改变结果或异常,只是执行一个动作。如果在 whenComplete 中自身抛出异常,它会被附加到已存在的异常之上。
示例:只记录日志,不干预结果
future
.whenComplete((res, ex) -> {
System.out.println("任务已完成。有错误? " + (ex != null));
})
.thenAccept(r -> System.out.println("给用户的结果:" + r));
5. 实现要点与细节
最佳实践:如何在 CompletableFuture 中正确处理错误
- 务必在异步任务链中添加错误处理(exceptionally、handle 或 whenComplete)。否则错误可能无人察觉,应用会出现不可预测的行为。
- 不要在主线程中使用 get() 或 join() 而不配合 try-catch —— 这会把异步代码变成同步,并可能导致阻塞。
- 如果需要在出错时返回“兜底”值——使用 exceptionally 或 handle。
- 对于副作用(记录日志、通知用户)——使用 whenComplete。
- 链路可以组合:例如,先用 exceptionally 处理错误,再用 whenComplete 记录日志,然后继续处理结果。
- 请记住,如果错误未被处理,它会“流向”后续对 get()/join() 的调用,可能导致应用崩溃。
方法的顺序
- 使用 exceptionally 时,它只拦截链中位于它之前发生的错误。
- 如果在 exceptionally 之后(例如在 thenApply 中)再次发生错误,需要单独处理。
- handle 是通用的——无论是否有错误,它都会触发。
方法的组合
CompletableFuture.supplyAsync(() -> {
// ...
})
.handle((result, ex) -> {
if (ex != null) return "错误:" + ex.getMessage();
return result;
})
.whenComplete((res, ex) -> {
System.out.println("任务已结束,结果:" + res);
});
如果不处理错误会怎样?
如果异常未被处理,而你调用了 get() 或 join(),它会被抛出为 ExecutionException(或 CompletionException),应用可能因此异常结束。
6. 在 CompletableFuture 中处理错误的常见误区
错误 1:缺少错误处理。 如果既没有 exceptionally、也没有 handle 或 whenComplete,错误会一直“沉睡”,直到某个很远的地方调用 get()/join() 才暴露。
错误 2:在主线程中不加 try-catch 就使用 get()/join()。 这会把异步代码变成同步,并可能导致阻塞或意外崩溃。
错误 3:误判处理器的作用范围。 exceptionally 只捕获位于它之前的错误。如果它之后再次出错,就不会被该方法处理。
错误 4:处理了错误却不返回值。 在 exceptionally 或 handle 中必须返回一个值,否则下一个阶段会拿到 null(或根本拿不到)。
错误 5:混淆 handle 与 whenComplete。 handle 可以改变结果,而 whenComplete 只能执行动作(例如记录日志)。如果你想改变结果——使用 handle。
错误 6:重复编写错误处理逻辑。 通常可以把错误处理集中在一个地方,避免重复代码——例如通过集中式的 handle 或公共处理器。
GO TO FULL VERSION