CodeGym /课程 /JAVA 25 SELF /异步代码中的错误处理:exceptionally、handle

异步代码中的错误处理:exceptionally、handle

JAVA 25 SELF
第 55 级 , 课程 3
可用

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 中正确处理错误

  • 务必在异步任务链中添加错误处理(exceptionallyhandlewhenComplete)。否则错误可能无人察觉,应用会出现不可预测的行为。
  • 不要在主线程中使用 get()join() 而不配合 try-catch —— 这会把异步代码变成同步,并可能导致阻塞。
  • 如果需要在出错时返回“兜底”值——使用 exceptionallyhandle
  • 对于副作用(记录日志、通知用户)——使用 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、也没有 handlewhenComplete,错误会一直“沉睡”,直到某个很远的地方调用 get()/join() 才暴露。

错误 2:在主线程中不加 try-catch 就使用 get()/join()。 这会把异步代码变成同步,并可能导致阻塞或意外崩溃。

错误 3:误判处理器的作用范围。 exceptionally 只捕获位于它之前的错误。如果它之后再次出错,就不会被该方法处理。

错误 4:处理了错误却不返回值。exceptionallyhandle 中必须返回一个值,否则下一个阶段会拿到 null(或根本拿不到)。

错误 5:混淆 handle 与 whenComplete。 handle 可以改变结果,而 whenComplete 只能执行动作(例如记录日志)。如果你想改变结果——使用 handle

错误 6:重复编写错误处理逻辑。 通常可以把错误处理集中在一个地方,避免重复代码——例如通过集中式的 handle 或公共处理器。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION