Java CompletableFuture.allOf().whenComplete() 有多个异常

Java CompletableFuture.allOf().whenComplete() with multiple exceptions

问题

在Java官方文档中,它说

public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)

Returns a new CompletableFuture that is completed when all of the given CompletableFutures complete. If any of the given CompletableFutures complete exceptionally, then the returned CompletableFuture also does so, with a CompletionException holding this exception as its cause.

文档没有指定多个给定的 CompletableFutures 异常完成的情况。比如下面的代码片段中,如果c1,c2,c3都异常完成,异常和原因是什么?

CompletableFuture.allOf(c1, c2, c3)
        .whenComplete((result, exception) -> {
            if (exception != null) {
                System.out.println("exception occurs");
                System.err.println(exception);
            } else {
                System.out.println("no exception, got result: " + result);
            }
        })

我的实验1

创建一个 completableFuture signal_1signal_2 都非常快地完成。输出显示 signal_1 作为异常原因传递给 .whenComplete()

package com.company;

import java.util.concurrent.*;

public class Main {

    private static void runTasks(int i) {
        System.out.printf("-- input: %s --%n", i);

        CompletableFuture<Void> signal_1 = new CompletableFuture<>();
        signal_1.completeExceptionally(new RuntimeException("Oh noes!"));

        CompletableFuture<Integer> signal_2 = CompletableFuture.supplyAsync(() -> 16 / i);

        CompletableFuture.allOf(signal_1, signal_2)
                .thenApplyAsync(justVoid -> {
                     final int num = signal_2.join();
                     System.out.println(num);
                     return num;
                })
                .whenComplete((result, exception) -> {
                    if (exception != null) {
                        System.out.println("exception occurs");
                        System.err.println(exception);
                    } else {
                        System.out.println("no exception, got result: " + result);
                    }
                })
                .thenApplyAsync(input -> input * 3)
                .thenAccept(System.out::println);

    }

    public static void main(String[] args) {
        runTasks(0);
    }

}

输出

-- input: 0 --
exception occurs
java.util.concurrent.CompletionException: java.lang.RuntimeException: Oh noes!

Process finished with exit code 0

我的实验二

signal_1 异常完成之前添加了 3 秒睡眠,因此 signal_1 应该在 signal_2 之后完成。但是,输出仍然显示 signal_1 被传递给 .whenComplete() 作为异常的原因。

package com.company;

import java.util.concurrent.*;

public class Main {

    static ExecutorService customExecutorService = Executors.newSingleThreadExecutor();

    private static void runTasks(int i) {
        System.out.printf("-- input: %s --%n", i);

        CompletableFuture<Void> signal_1 = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            throw new RuntimeException("Oh noes!");
        }, customExecutorService);

        CompletableFuture<Integer> signal_2 = CompletableFuture.supplyAsync(() -> 16 / i);

        CompletableFuture.allOf(signal_1, signal_2)
                .thenApplyAsync(justVoid -> {
                     final int num = signal_2.join();
                     System.out.println(num);
                     return num;
                })
                .whenComplete((result, exception) -> {
                    if (exception != null) {
                        System.out.println("exception occurs");
                        System.err.println(exception);
                    } else {
                        System.out.println("no exception, got result: " + result);
                    }
                })
                .thenApplyAsync(input -> input * 3)
                .thenAccept(System.out::println);

    }

    public static void main(String[] args) {
        runTasks(0);
        customExecutorService.shutdown();
    }

}

输出

-- input: 0 --
exception occurs
java.util.concurrent.CompletionException: java.lang.RuntimeException: Oh noes!

Process finished with exit code 0

这在很大程度上重复了 VGR 在评论中所说的话,但这是一个重要的经验法则,值得完整 write-up。

在Java中,有一个重要的概念叫做Unspecified Behaviour。简而言之,如果文档没有明确定义在特定场景中发生的事情,那么实现可以在合理的范围内并在明确定义的其他规则的范围内自由地做它选择的任何事情。这很重要,因为它有几种不同的表现形式。

对于初学者来说,结果可能是平台特定的。对于某些机器,将行为保留为未定义允许进行一些特殊优化,这些优化仍然 return “正确”的结果。由于 Java 优先考虑所有平台上的 similar/same 行为和性能,选择不指定执行的某些方面允许他们忠实于该承诺,同时仍然获得特定平台带来的优化优势.

另一个例子是当将行为统一为特定动作的行为目前不可行时。如果非要我猜的话,这很可能就是 Java 实际在做的事情。在某些情况下,Java 会设计一个具有 某些功能潜力 的组件,但不会实际定义和实现它。这通常是在构建一个完整的解决方案比其价值更多的努力以及其他原因的情况下完成的。具有讽刺意味的是,CompletableFuture 本身就是一个很好的例子。 Java 5 介绍了 Future 的,但只是作为具有通用功能的接口。 CompletableFuture 是 Java 8 中的实现,后来充实并定义了 Java 5 接口遗留下来的所有未指定行为。

最后,如果选择特定行为会扼杀可能实现的灵活性,他们可能会避免定义特定行为。目前,您展示的方法没有关于 的任何指定行为,当期货失败时将抛出 异常。这允许以后扩展 CompletableFuture 的任何 class 能够为自己指定该行为,同时仍保持 Liskov's Substitution Principle。如果您还不知道,LSP 说如果子 class 扩展了父 class,那么子 class 必须遵循父 class 的所有规则。因此,如果 class 的规则(指定行为)过于严格,那么您将阻止此 class 的未来 implementations/extensions 在不破坏 LSP 的情况下运行。 CompletableFuture 可能有一些扩展,允许您准确定义调用方法时抛出的异常类型。但这就是重点 - 它们是您可以选择 opt-in 的扩展。如果他们为您定义了它,那么除非您自己实现它,否则您将坚持使用它,或者您离开语言标准库。