简体   繁体   中英

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

Problem

In Java Official Doc, it says

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.

The doc doesn't specify the case when multiple given CompletableFutures complete exceptionally. For example, in the following code snippet, what will the exception and its cause be if c1, c2, c3 all complete exceptionally?

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);
            }
        })

My experiment 1

Create a completableFuture signal_1 and signal_2 that both completes exceptionally fast. The output shows signal_1 gets passed to .whenComplete() as the cause of exception.

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);
    }

}

Output

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

Process finished with exit code 0

My experiment 2

Added a 3 second sleep before signal_1 completes exceptionally, so signal_1 should completes after signal_2 . However, the output still shows signal_1 gets passed to .whenComplete() as the cause of exception.

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();
    }

}

Output

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

Process finished with exit code 0

This is largely a repeat of what VGR said in the comments, but it is an important rule of thumb that deserves a full write-up.

In Java, there is an important concept called Unspecified Behaviour . In short, if the docs do not explicitly define what happens in a specific scenario, then the implementation is free to do literally whatever it chooses to, within reason and within the bounds of the other rules that are explicitly defined. This is important because there are several different manifestations of that.

For starters, the result could be platform specific. For some machines, leaving the behaviour undefined allows for some special optimizations that still return a "correct" result. And since Java prioritizes both similar/same behaviour on all platforms as well as performance, choosing not to specify certain aspects of execution allows them to stay true to that promise while still getting the optimization benefits that come with specific platforms.

Another example is when the act of unifying behaviour into a specific action is not currently feasible. If I had to guess, this is most likely what Java is actually doing. In certain instances, Java will design a component with the potential for certain functionality, but will stop short of actually defining and implementing it. This is usually done in instances where building out a full blown solution would be more effort than it is worth, amongst other reasons. Ironically enough, CompletableFuture itself is a good example of this. Java 5 introduced Future's, but only as an interface with generic functionality. CompletableFuture, the implmementation which came in Java 8, later fleshed out and defined all the unspecified behaviour left over from the Java 5 interface.

And lastly, they may avoid defining specified behaviour if choosing specified behaviour would stifle the flexibility of possible implementations. Currently, the method you showed does not have any specified behaviour about which exception will be thrown when the futures fail. This allows any class that later extends CompletableFuture to be able to specify that behaviour for themselves while still maintaining Liskov's Substitution Principle . If you don't already know, LSP says that if a Child class extends a Parent class, then that Child class must follow all the rules of the Parent class. As a result, if the rules (specified behaviour) of the class are too restrictive, then you prevent future implementations/extensions of this class from being able to function without breaking LSP. There are likely some extensions for CompletableFuture that allow you to define exactly what type of Exception is thrown when calling the method. But that's the point - they are extensions that you can choose to opt-in to. If they define it for you, then you are stuck with it unless you implement it yourself, or you go outside the languages standard library.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM