1. 简介

Java 8 引入了基于 Future 的新抽象——CompletableFuture 类,主要用于执行异步任务。它的出现是为了解决传统 Future API 的各种痛点。

本文将深入探讨在使用 CompletableFuture 时处理异常的几种方式。

2. CompletableFuture 快速回顾

先简单回顾 CompletableFuture 的核心概念:它是 Future 的实现类,支持执行和链式调用异步操作。异步任务通常有三种完成状态:

  • ✅ 正常完成
  • ❌ 异常完成
  • ⚠️ 被外部取消

CompletableFuture 提供了丰富的 API 方法应对这些场景。与其他方法类似,异常处理方法也提供同步、异步和指定线程池的异步版本。下面我们逐一分析异常处理方案。

3. handle() 方法

handle() 方法允许我们访问并转换整个 CompletionStage 的结果,无论成功还是失败。它接受一个 BiFunction函数式接口,包含两个参数:

  1. 前一阶段的结果
  2. 可能发生的 Exception

⚠️ 关键点:**这两个参数都可能为 null**(正常完成时异常为 null,异常完成时结果为 null)。

示例代码:

@ParameterizedTest
@MethodSource("parametersSource_handle")
void whenCompletableFutureIsScheduled_thenHandleStageIsAlwaysInvoked(int radius, long expected)
  throws ExecutionException, InterruptedException {
    long actual = CompletableFuture
      .supplyAsync(() -> {
          if (radius <= 0) {
              throw new IllegalArgumentException("Supplied with non-positive radius '%d'");
          }
          return Math.round(Math.pow(radius, 2) * Math.PI);
      })
      .handle((result, ex) -> {
          if (ex == null) {
              return result;
          } else {
              return -1L;
          }
      })
      .get();

    Assertions.assertThat(actual).isEqualTo(expected);
}

static Stream<Arguments> parameterSource_handle() {
    return Stream.of(Arguments.of(1, 3), Arguments.of(1, -1));
}

核心特点:handle() 总是会执行,返回新的 CompletionStage,并通过 get() 获取其转换后的结果。

4. exceptionally() 方法

当只需要处理异常场景时,handle() 显得不够灵活。此时可用 exceptionally()——仅在前一阶段异常完成时执行回调。若无异常,则跳过回调继续执行后续链。

示例代码:

@ParameterizedTest
@MethodSource("parametersSource_exceptionally")
void whenCompletableFutureIsScheduled_thenExceptionallyExecutedOnlyOnFailure(int a, int b, int c, long expected)
  throws ExecutionException, InterruptedException {
    long actual = CompletableFuture
      .supplyAsync(() -> {
          if (a <= 0 || b <= 0 || c <= 0) {
              throw new IllegalArgumentException(String.format("Supplied with incorrect edge length [%s]", List.of(a, b, c)));
          }
          return a * b * c;
      })
      .exceptionally((ex) -> -1)
      .get();

    Assertions.assertThat(actual).isEqualTo(expected);
}

static Stream<Arguments> parametersSource_exceptionally() {
    return Stream.of(
      Arguments.of(1, 5, 5, 25),
      Arguments.of(-1, 10, 15, -1)
    );
}

⚠️ 注意:若异常已被 handle() 捕获,后续的 exceptionally() 不会执行

@ParameterizedTest
@MethodSource("parametersSource_exceptionally")
void givenCompletableFutureIsScheduled_whenHandleIsAlreadyPresent_thenExceptionallyIsNotExecuted(int a, int b, int c, long expected)
  throws ExecutionException, InterruptedException {
    long actual = CompletableFuture
      .supplyAsync(() -> {
          if (a <= 0 || b <= 0 || c <= 0) {
              throw new IllegalArgumentException(String.format("Supplied with incorrect edge length [%s]", List.of(a, b, c)));
          }
          return a * b * c;
      })
      .handle((result, throwable) -> {
          if (throwable != null) {
              return -1;
          }
          return result;
      })
      .exceptionally((ex) -> {
          System.exit(1); // 这行不会执行
          return 0;
      })
      .get();

    Assertions.assertThat(actual).isEqualTo(expected);
}

5. whenComplete() 方法

whenComplete() 接受 BiConsumer 参数(结果和异常),但与前两者有本质区别:它不会转换异常结果。即使回调总是执行,前一阶段的异常仍会继续传播。

示例代码:

@ParameterizedTest
@MethodSource("parametersSource_whenComplete")
void whenCompletableFutureIsScheduled_thenWhenCompletedExecutedAlways(Double a, long expected) {
    try {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        long actual = CompletableFuture
          .supplyAsync(() -> {
              if (a.isNaN()) {
                  throw new IllegalArgumentException("Supplied value is NaN");
              }
              return Math.round(Math.pow(a, 2));
          })
          .whenComplete((result, exception) -> countDownLatch.countDown())
          .get();
        Assertions.assertThat(countDownLatch.await(20L, java.util.concurrent.TimeUnit.SECONDS));
        Assertions.assertThat(actual).isEqualTo(expected);
    } catch (Exception e) {
        Assertions.assertThat(e.getClass()).isSameAs(ExecutionException.class);
        Assertions.assertThat(e.getCause().getClass()).isSameAs(IllegalArgumentException.class);
    }
}

static Stream<Arguments> parametersSource_whenComplete() {
    return Stream.of(
      Arguments.of(2d, 4),
      Arguments.of(Double.NaN, 1)
    );
}

关键现象:第二次测试中,whenComplete() 虽执行了,但 IllegalArgumentException 被包装成 ExecutionException 抛出。

6. 未处理异常

未捕获的异常会导致 CompletableFuture 以异常状态完成,但不会直接传播到调用方。前文示例中 get() 抛出 ExecutionException,是因为我们尝试获取异常完成的结果。

检查 CompletableFuture 状态的方法:

  • isCompletedExceptionally() / isCancelled() / isDone()(布尔值)
  • state()(返回 State 枚举,如 RUNNING/SUCCESS

建议在调用 get() 前检查状态,避免踩坑。

7. 总结

本文系统分析了 CompletableFuture 中处理异常的三种核心方法:

  1. handle():万能型,总是执行并转换结果
  2. exceptionally():异常专用,简洁高效
  3. whenComplete():仅做副作用,不处理异常传播

完整源码见 GitHub 仓库


原始标题:Working with Exceptions in Java CompletableFuture