CompletableFuture
is a plain Java class. It represents the result of an asynchronous computation. A future object is something which will become attainable in the future. When the asynchronous computation finishes, the future is completed.
Introduced in Java 8, CompletableFuture implements the older Future and adds support for callbacks, mapping operations, and cleaner awaiting of completion. If you are using Java 9 or later, you can also use delays and timeouts, subclass CompletableFuture to change the default executor, and take advantage of more utility methods.
Using callbacks allows you to run a task when a future completes. The simplest callbacks are thenRun
and thenAccept
, the latter of which allows you to use the result of the future in your callback.
This is a callback:
CompletableFuture<MyResult> future = // ...
future.thenAccept((result) -> {
// Here, you have a MyResult object
// you can call methods on MyResult as you normally would
result.sayHi();
});
Other CompletableFuture methods allow you to transform a future of one object into a future of another. The most simple is thenApply
. When the first future is completed, its result is fed to the mapping function. Once that mapping function returns, the second future is completed with its result.
In that case, you need to accept that you have a CompletableFuture and not the Object itself. You should continue your logic
inside a callback, using the thenAccept
method. Let's use the example of CompletableFuture<Foo>
:
CompletableFuture<Foo> futureFoo = // ...
futureFoo.thenAccept((foo) -> {
// now you have the Foo
System.out.println("The foo is " + foo);
System.out.println("This message appears after futureFoo is completed");
});
When the future is completed, all callbacks are triggered. Usually callbacks use lambdas, but this is no requirement.
If you don't care about the value of the CompletableFuture when it completes, you can use the thenRun
method.
This is a little bit more complicated. Sometimes you need to return a result which involves the Foo
, suppose we call it Bar
. You have written a partially complete method, but you don't now how to return Bar
.
public Bar getBar() {
CompletableFuture<Foo> futureFoo = // ...
// How do I get "theFoo"?
return new Bar(theFoo);
}
If you absolutely must return a Bar
immediately, there is nothing you can do except block the current thread and wait for the future to complete. You do this with CompletableFuture.join()
.
public Bar getBar() {
CompletableFuture<Foo> futureFoo = // ...
Foo theFoo = futureFoo.join();
return new Bar(theFoo);
}
However, this approach has its drawbacks. You do not know how long futureFoo
may take to complete. Maybe it's a database request which takes seconds to finish because some user's server has a MySQL database on the other side of the world. Using join will block the current thread until the future is complete.
The other way is to change the return type of your method to CompletableFuture<Bar>
, and use one of the mapping operations:
public CompletableFuture<Bar> getBar() {
CompletableFuture<Foo> futureFoo = // ...
CompletableFuture<Bar> result = futureFoo.thenApply((theFoo) -> {
return new Bar(theFoo);
});
return result;
}
Now, you have transformed a future Foo
into a future Bar
using thenApply. However, this approach may require some refactoring. You might need to change the return types of other methods relying on this one.
You will notice three variants of all methods made for callback and mapping operations. For example:
thenAccept(Consumer<T>)
thenAcceptAsync(Consumer<T>)
thenAcceptAsync(Consumer<T>, Executor)
These methods are actually best explained in reverse direction
The third variant of the methods accepts the operation you want to run, and an Executor in which to run it. The Executor determines on which thread the operation is run. You can supply the thread pool of your choice. For example,
Executor executor;
ExecutorService executorService = Executors.newFixedThreadPool(5); // Creates a thread pool of 5 in size
// Who says CompletableFuture needs to be used for everything? You can execute Runnables in the Executor directly
executor.execute(() -> {
// do something here
});
CompletableFuture<FirstResult> firstFuture = CompletableFuture.supplyAsync(() -> {
return new FirstResult();
}, executor);
CompletableFuture<SecondResult> secondFuture = futureFromPool.thenApplyAsync((firstResult) -> {
return new SecondResult(firstResult);
}, executor);
executorService.shutdown();
Remember to shutdown any ExecutorServices you create when you are done using them. In this example the ExecutorService has a local scope, but for most use cases you will store one in a field.
The second variant is similar to the third variant, however, it chooses the ExecutorService for you. This way, you don't have to worry too much about shutting down an ExecutorService.
Internally, the second variant uses ForkJoinPool.commonPool()
as the Executor (You can change the default executor in Java 9).
But this is not free - you shouldn't use this variant for all methods. If your code is blocking (file IO, network operations), you should use your own ExecutorService. The common pool is intended only for computational work.
The first variant of method is intended for small and lightweight operations. It can run in any thread, including the calling thread or the thread completing the future. You should not rely on the thread in which your operation runs, therefore it is suitable when you have no care for which thread the operation runs in.
You may have noticed the symmetry of much of this tutorial:
- What are callbacks?
- Operations which run when a future completes
- What are mapping operations?
- Operations which run when a future completes, which map that future to a new kind of future
- How do I use a CompletableFuture returned from another method?
- The answer is to use a callback
- How do I return a value using a CompletableFuture?
- The answer is to use a mapping operation