Oscar Ablinger
Oscar Ablinger

Oscar Ablinger

The ultimate guide to Futures in Java and Guava

The ultimate guide to Futures in Java and Guava

Untangling the mess of Futures

Oscar Ablinger
·Aug 24, 2021·

6 min read

Subscribe to my newsletter and never miss my upcoming articles

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 Futures 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 new CompletableFuture that is returned from this method.

  • exceptionally does the same as thenAccept, except that it is called when the CompletableFuture 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 the CompletableFuture 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 the CompletableFuture once finished.

  • completedFuture will immediately return a successfully completed future. It’s counterpart failedFuture 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 a FluentFuture that will complete once all of the steps defined by the ClosingFuture 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.

 
Share this