The ultimate guide to Futures in Java and Guava
Untangling the mess of Futures
In Java 8 CompletableFuture
was introduced to finally bring a better way to work with asynchronous logic into Java. Meanwhile Guava has created its own solutions for this problem.
Now, which one should you use?
First let me start by pointing out that Guava has many classes that interact with futures and are meant to be used along-side each other. So most of those classes don’t necessarily step on each others toes.
But first, let’s look at the vanilla Java implementations:
Title photo by Drew Graham on Unsplash
Future
Future
is the base interface and is used by Java’s own CompletableFuture
and most of the interfaces and classes of Guava. It’s been around since Java 5.
The interface is very rudimentary, only offering methods to check its status, cancel it or gett the result in a blocking way (optionally with timeout). This works for the simplest cases, but if you want to work with multiple Future
s at once or maybe even chain operations, this interface alone quickly becomes tedious.
Some example code taken from the java documentation:
interface ArchiveSearcher { String search(String target); }
class App {
ExecutorService executor = ...
ArchiveSearcher searcher = ...
void showSearch(final String) throws InterruptedException {
Future<String> future
= executor.submit(new Callable<String>() {
public String call() {
return searcher.search(target);
}});
displayOtherThings(); // do other things while searching
try {
displayText(future.get()); // use future
} catch (ExecutionException ex) { cleanup(); return; }
}
}
CompletableFuture
In Java 8 CompletableFuture
and its interface CompletableTask
were added to allow more powerful operations with futures. Those two are so tightly coupled that the interface can essentially be ignored.
CompletableFuture
implements Future
and also offers some more methods to work with the result:
thenAccept
allows you to register a function that will be called upon completion and whose result will be used for a newCompletableFuture
that is returned from this method.exceptionally
does the same asthenAccept
, except that it is called when theCompletableFuture
is completed exceptionally.handle
allows you to handle both the success value as well as the exception in the same function.join
will block until theCompletableFuture
completes and either return the value or throw any of the occurring exceptions.
It also offers you some convenience methods to create CompletableFutures
:
supplyAsync
let’s you specify a Supplier that will be called asynchronously and whose result will be the result of theCompletableFuture
once finished.completedFuture
will immediately return a successfully completed future. It’s counterpartfailedFuture
was added in Java 9.
Some example code:
public CompletableFuture<List<User>> loadActiveUsers() {
return httpClient.<List<User>>sendAsync(request, bodyHandler)
.thenApply(this::filterListOfUsersForActiveOnes)
.exceptionally(throwable -> {
LOG.warn(throwable);
return List.of();
});
}
public void displayListOfActiveUsers() {
try {
CompletableFuture<> activeUsersFuture = loadActiveUsers();
displayOtherStuff();
try {
displayActiveUsers(activeUsersFuture.join());
} catch (CompletionException e) {
LOG.error(e);
displayNoUsers();
}
}
}
Problems with CompletableFuture
While the fluent style of CompletableFuture
is certainly a great improvement to the bare-bones approach of Future
, it still has its quirks.
For one it’s easy to forget that it can also be canceled. This means that if you return a CompletableFuture
the caller can cancel it! This might leave you with resources not being properly closed or an unexpected state. The easiest way to avoid this is to wrap your CompletableFuture
in another one.
Another problem is also that exceptions are handled slightly inconsistently, which the documentation doesn’t mention:
NullPointerException exception = new NullPointerException(":(");
CompletableFuture.failedFuture(exception)
.exceptionally(throwable -> {
// here throwable is the exception from above
return null;
});
CompletableFuture.failedFuture(exception)
.thenApply(Object::toString)
.exceptionally(throwable -> {
// here throwable is a CompletionException that wraps the
// exception from above
return null;
});
Lastly, handling only specific exceptions isn’t that easy. The function in exceptionally
receives an instance of Throwable
, which is a checked exception. This means you can’t re-throw it without first wrapping it in a RuntimeException
, preferably a CompletionException
.
While a CompletionException
thrown in one of those methods is directly returned (for instance when using join
), nested ones are not unwrapped. This means that you’ll likely end up with some boilerplate code like this:
.exceptionally(throwable -> {
while (throwable instanceof CompletionException) {
throwable = throwable.getCause();
}
if (throwable instanceof NullPointerException) {
// handle the exception I care about
return null;
} else {
throw new CompletionException(throwable);
}
})
Futures
Moving away from the Java standard library, Guava’s static class Futures
contains methods to more effectively work with Futures. It does so mostly by utilizing the ListenableFuture
interface that we’ll look at next.
Notably, however, it also has a transform
method that allows you to create a new future based on another one and a function. The new future will complete with the result of the provided one being transformed by the given function. This essentially mimics the way that CompletableFuture.thenApply
works.
This class should be your first stop when trying to work with futures and Guava!
ListenableFuture
ListenableFuture
is a simple interface in Guava that implements Future
and only adds one method: addListener
.
This method allows you to register listeners that are called once the future completes. Albeit they do not directly get the result, you may get it using the get
method of the ListenableFuture
itself.
This interface is the main one utilized in the Futures
class. It is also implemented by most of Guava’s future classes.
ListenableFutureTask
This class is essentially a bare-bones implementation of the ListenableFuture
interface. It has two factory methods using Callable
or Runnable
but otherwise only implements the methods of its interface.
SettableFuture
SettableFuture
extends the methods of the ListenableFuture
interface with setter methods. You can set its success value, exception or even set it based on another future.
This class is mainly useful for when you already have a thread on which you want to complete your future.
AbstractFuture
As the name implies AbstractFuture
is an abstract implementation of the ListenableFuture
interface. Beyond the already mentioned addListener
and setters, it also has some methods that expose its internal state.
It’s unlikely that you’ll need this class, but it can be used to implement your own versions of ListenableFuture
.
FluentFuture
FluentFuture
is the most similar to Java’s CompletableFuture
. It implements ListenableFuture
and also offers mapping methods in transform
and catching
.
In this case catching
allows catching specific exceptions, not just a Throwable
. This enables a more selective approach to exception handling where the theoretical possibility of checked exceptions doesn’t result in a lot of boilerplate code.
Some example code taken from its documentation:
ListenableFuture<Boolean> adminIsLoggedIn =
FluentFuture.from(usersDatabase.getAdminUser())
.transform(User::getId, directExecutor())
.transform(ActivityService::isLoggedIn, threadPool)
.catching(RpcException.class, e -> false, directExecutor());
ClosingFuture
ClosingFuture
is similar to FluentFuture
, but acts more as a builder than a future itself. It offers methods to transform values, catch exceptions and combining multiple of them. At the end a ClosingFuture
always has to be “finished”.
There is currently two ways you can finish it:
finishToFuture
will return aFluentFuture
that will complete once all of the steps defined by theClosingFuture
are completed.finishToValueAndCloser
will call a callback once finished.
An example from its documentation:
FluentFuture<UserName> userName =
ClosingFuture.submit(
closer -> closer.eventuallyClose(database.newTransaction(), closingExecutor),
executor)
.transformAsync((closer, transaction) -> transaction.queryClosingFuture("..."), executor)
.transform((closer, result) -> result.get("userName"), directExecutor())
.catching(DBException.class, e -> "no user", directExecutor())
.finishToFuture();
Conclusion
When working with vanilla Java, your best bet is CompletableFuture
. Once you also have Guava, you have a myriad of options to choose from. All of them fill slightly different niches and are meant for different levels of detail.
In the end, Guava’s many classes can quickly become overwhelming and confusing.
Personally, I’ll probably try out ClosingFuture
and FluentFuture
a bit more, but stick with CompletableFuture
for now — mainly because I can safely stick it in APIs without having to worry about introducing a new dependency.