The Future is Completable in Java 8
As I mentioned in my previous post, interface Future in Java up to version 7 has a serious limitation, and that is that it’s not possible to get notified when the result is available. The only thing you can do with a Future is call get() on it, which will block if the result is not yet available, or you can check if it’s ready (polling) by calling the isDone() method.
But both of these things are not what you want – if you want your application to be scalable you don’t want to waste resources by issuing blocking calls, and polling also not ideal. What you really want is attach a callback to the Future to process the result when it’s available.
In Java 8, a new and powerful implementation of interface Future was added: class CompletableFuture, which allows you to attach callbacks and much more – in fact, it allows you to build a pipeline of steps which can each be executed asynchronously, each step depending on the result of one or more previous steps.
The API of CompletableFuture is quite elaborate, it’s API documentation specifies no less than 59 methods, so it’s not easy to understand when you should use which of the methods. In this post I’ll organize and explain the API of CompletableFuture.
Let’s first have a look at CompletionStage, the superinterface of CompletableFuture, which declares most of the methods that comprise the functionality of CompletableFuture.
According to the API documentation, a CompletionStage is:
A stage of a possibly asynchronous computation, that performs an action or computes a value when another CompletionStage completes. A stage completes upon termination of its computation, but this may in turn trigger other dependent stages.
An example of what this might be useful for: Suppose you have an online service which needs to do a database query and a call to a web service and combine the results of both into a web page. You could do this in the straightforward way: first do a database query, then send a request to the web service and then assemble the results into a web page. It would be faster, however, if you could issue the database query and the request to the web service concurrently, and then assemble the web page when both return a result. You can do that with completion stages.
Organization of interface CompletionStage
The interface defines 38 methods, which are organized in 3 blocks of 3 methods which each have 3 variants, plus four more general handler methods (of which three come in 3 variants) and one other method: (3 x 3 x 3) + (3 x 3 + 1) + 1 = 38 methods.
The grouping of the 3 x 3 x 3 methods is done by the following orthogonal aspects:
- Whether the stage is triggered by the completion of a single previous stage, the completion of both of two previous stages (AND), or the completion of either of two previous stages (OR).
- Whether the computation takes an argument or not and returns a result or not.
- How the execution of the computation is arranged (synchronous or asynchronous, or with a custom Executor).
The names of the methods reflect which of these aspects applies to each method (although the naming is not entirely consistent).
The first aspect (what triggers a stage):
- Methods with names starting with “then” are for adding another stage to be triggered when a single stage completes.
- Methods with names containing “both” are for adding another stage to be triggered when two previous stages both complete.
- Methods with names containing “either” are for adding another stage to be triggered when either one of two previous stages completes.
The second aspect (whether the computation takes an argument and returns a result):
- Methods with names containing “apply” take a Function, which takes an argument (the result of the previous stage) and return a result (the argument for the next stage).
- Methods with names containing “accept” take a Consumer, which takes an argument but does not return a result.
- Methods with names containing “run” take a Runnable, which takes no arguments and does not return a result.
The third aspect (how the execution of the computation is arranged):
- Methods with names which do not end in “async” execute the computation using the stage’s default execution facility.
- Methods with names that end in “async” execute the computation using the stage’s default asynchronous execution facility.
- Methods with names that end in “async” and that also take an Executor argument, execute the computation using the specified Executor.
How exactly a computation runs with the “default” and “default asynchronous” execution model is not specified by interface CompletionStage, but is left up to the implementation.
For CompletableFuture, “default” means: execute in the thread that completes the previous stage, and “default asynchronous” means: execute using the default thread pool of the fork-join framework (the executor returned by ForkJoinPool.commonPool()).
Block 1 – triggered by a single previous stage
|thenApply(Async)||Function||CompletionStage holding the result of the Function|
Block 2 – triggered by both of two previous stages
Each of these methods take another CompletionStage and one of: BiFunction, BiConsumer or Runnable.
|thenCombine(Async)||BiFunction||CompletionStage holding the result of the Function|
Note the irregularity in the method names here: if the naming would have been regular, thenCombine should have been named applyToBoth and thenAcceptBoth should have been named acceptBoth. The irregularity is curious, especially since in block 3 the methods do have the expected names.
Note about exception handling: When both the previous stages result in an exception, then it’s not specified which of the two exceptions gets propagated to the dependent stage.
Block 3 – triggered by either one of two previous stages
Each of these methods take another CompletionStage and one of: Function, Consumer or Runnable.
|applyToEither(Async)||Function||CompletionStage holding the result of the Function|
Note about exception handling: It’s not specified which result of the previous two stages is propagated to the dependent stage. For example, if one previous stage results in an exception and the other one results in a value, then the dependent stage could receive either an exception or a value.
More general handler methods
Besides the three blocks, there are four more general handler methods, that also let you deal with exceptions that might be thrown during a computation.
The thenCompose(Async) methods (3 variants) are similar to thenApply(Async), except that the Function passed to thenCompose returns a CompletionStage containing the result of the computation instead of returning the result itself. You’ll most likely not need to use thenCompose a lot.
The whenComplete(Async) methods (3 variants) take a BiConsumer which is passed two arguments: the value returned by the previous stage and a Throwable. One of these two arguments will contain a value and the other one will be null, depending on whether the previous step completed normally or with an exception. The CompletionStage returned by whenComplete preserves the result of the previous step (unless the BiConsumer throws an exception and the previous stage didn’t throw an exception – then the returned CompletionStage is completed with the exception thrown by the BiConsumer).
The handle(Async) methods (3 variants) are almost the same as the whenComplete(Async) methods, except that they take a BiFunction instead of a BiConsumer, and instead of preserving the result of the previous stage, they return a CompletionStage with the result of the specified BiFunction.
Finally, there’s the method exceptionally (of which there is only one variant, not three) specifically for dealing with exceptions. It takes a Function which gets the exception as its argument, and which returns a value to be returned instead of the exception.
The final method
The last method is toCompletableFuture, which returns the CompletionStage as a CompletableFuture.
Interface CompletionStage specifies most of the functionality of CompletableFuture. Class CompletableFuture adds a number of extra methods:
- Implementations for the five methods of interface Future.
- Methods complete and completeExceptionally, to complete the future with a value or an exception.
- Somewhat dangerous methods obtrudeValue and obtrudeException, to overwrite the result of the future even when it was previously already completed with a value or exception. (Dangerous because other threads might already have seen the previous result – using these methods could lead to hard to pin down concurrency bugs).
- A method getNow which doesn’t block, but returns a fallback value when the result is not yet available.
- A method join which is the same as get, except that it doesn’t throw any checked exceptions, which makes it more useful for use in lambda expressions.
- A method isCompletedExceptionally which checks if the future was completed with an exception.
- Three of static factory methods: supplyAsync, runAsync, completedFuture.
- Two methods to combine multiple futures into one: allOf, anyOf.
As you can see, CompletionStage and CompletableFuture provide a rich toolbox for combining steps that can be executed asynchronously and concurrently.
Download the CompletionStage Quick Reference!