When Java 8 was released a while ago, a great concurrency tool was added, the CompletableFuture class. The
CompletableFuture is a Future that can have it’s value explicity set and more interestingly can be chained together to support dependent actions triggered by the CompletableFutures completion. CompletableFutures are analogous to the ListenableFuture class found in Guava. While the two offer similar functionality, there won’t be any comparisons done in this post. I have previously covered ListenableFutures in this post. While the coverage of ListenableFutures is a little dated, most of the information should still apply. The documentation for the CompletableFuture class is comprehensive, but lacks concrete examples of how to use them. My goal is show how to use CompletableFutures through a series of simple examples in unit tests. Originally I was going to cover the CompleteableFuture in one post, but there is so much information, it seems better to break up coverage into 3 parts –
- Creating/combining tasks and adding listeners for follow on work.
- Handling errors and error recovery
- Canceling and forcing completion.
Before we dig into using CompleteableFutures, some background information is needed. The CompleteableFuture implements the CompletionStage interface. The javadoc consisely explains what the 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.
The full documentation for the CompletionStage is too long to include here, so we’ll briefly summarize the key points:
- Computations can be be represented by a Future, Consumer or a Runnable with the respective method names of apply, accept or run
- Execution of computations can be one of the following
- Default execution (possibly the calling thread)
- Async execution using the default async execution provider of the CompletionStage. These methods are denoted by the form of someActionAsync
- Async execution by using a provided Executor. These methods also follow the form of someActionAsync but take an
Executorinstance as an additonal parameter.
For the rest of this post I will be refering to
Creating A CompleteableFuture
Creating a CompletableFuture is simple, but not always clear. The simplest way is the
CompleteableFuture.completedFuture method which returns an a new, finished CompleteableFuture:
1 2 3 4 5 6
As unexciting as this may seem, the ability to create an already completed CompleteableFuture can come in handy as we’ll see a little later.
Now let’s take a look at how to create a
CompletableFuture that represents an asynchronous task:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
In the first code sample we see an example of a
runAsync task and the second sample is an example of
supplyAsync. This may be stating the obvious, but the decision to use supplyAsync vs runAsync is determined by whether the task is expected to return a value or not. In both examples here we are supplying a custom
Executor which is the asynchronous execution provider. When it comes to the supplyAsync method I personally think it would have been more natural to use a Callable and not a
Supplier. While both are functional interfaces, the
Callable is associated more with asynchronous tasks and can throw checked exceptions while the
Supplier does not (although with a small amount of code we can have Suppliers that throw checked exceptions).
Now that we can create
CompletableFuture objects to run asynchronous tasks, let’s learn how to ‘listen’ when a task completes to perform follow up action(s). It’s important to mention here that when adding follow on
CompletionStage objects, the previous task needs to complete successfully in order for the follow on task/stage to run. There are methods to deal with failed tasks, but handling errors in the
CompletableFuture chain are covered in a follow up post.
1 2 3 4 5 6 7 8 9 10 11
Here in this example we are running a task that “cleans up” after the first
CompletableFuture finishes sucessfully. While the previous example used a
Runnable task to execute after the original task completed successfully, there really is no connection between the two. We can also specify a follow on task that takes the result of the previous successful task directly:
1 2 3 4 5 6 7 8 9 10
This is an example of Accept methods that take the result of the
CompletableFuture and pass it to a
Consumer object. In Java 8
Consumer instances have no return value and are expected to work by side-effects, in this case adding the result to a list.
Combining And Composing Tasks
In addition to adding listeners to run follow up tasks or accept the results of a succesful CompletableFuture, we can combine and/or compose tasks.
Composing means taking the results of one successful CompletableFuture as input to another CompletableFuture via a Function.
Here’s an example of
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
In this example the first CompletionStage is providing a list of 10 multiples of a number, 13 in this case. The supplied Function takes those results and creates another CompletionStage which then sums the list of numbers.
Combining is accomplished by taking 2 successful CompletionStages and having the results from both used as parameters to a BiFunction to produce another result. Here’s a very simple example to demonstrate taking results from combined CompletionStages.
1 2 3 4 5 6 7 8 9 10
While the previous example showed combining two CompletionStages that could be asynchronous tasks, we could also combine an asynchronous task with an already completed CompletableFuture. It is good way to combine a known value with a value that needs to be computed:
1 2 3 4 5 6 7 8 9 10
Finally here’s an example of using the
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
Listening For The First Finished Task
In all of the previous examples final results required waiting for all CompletionStages to finish, but this doesn’t always need to be the case. We can get results from which ever task completes first. Here’s an example where the first completed result is accepted using a
1 2 3 4 5 6 7 8 9 10 11 12 13
And the analogous
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
Up to this point, all combining/composing examples have been two CompletableFuture objects only. This was done intentionally in an effort to make the examples more clear. But we can nest an arbitrary number of CompletionStages together. Please note that the following example is for illustration purposes only!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
It’s important to note that ordering is not guarateed when combining CompletionStages. In these unit tests, times were provided to the simulated tasks to ensure completion order.
This wraps up the first part of using the CompletableFuture class. In upcoming posts we’ll cover error handling/recovery and forcing completion/cancellation.