The Joys of Guava: ListenableFuture

The Guava library, for me, is hands down the best Java utility library out there. It is a shining example of clean code and API design, both in its implementation and documentation. If you’re not already using it in your Java projects – you absolutely should be! Check out the wiki here to get started.

In this series I want to highlight different pieces of Guava that I have found particularly useful. Often there’s a Guava utility that fits your need perfectly, so becoming familiar with the different features of the library can be extremely helpful!

Futures

Any Java developer who has dealt with concurrency will be familiar with the Future API. As useful as it is for obtaining results from or cancelling asynchronous computations, there are certain situations where the API leaves few features to be desired.

Background

Recall that Futures most commonly come from calls to ExecutorService.submit(). That return value is the subtle distinction between the submit and execute methods:

public interface Executor
{
    void execute(Runnable command);
}

public interface ExecutorService extends Executor
{
    Future<?> submit(Runnable task);
    
    ...
}

Usually when a thread exits due to an uncaught exception, the JVM reports this event to an application-provided UncaughtExceptionHandler. But there’s more to it than that. Java Concurrency in Practice explains it in detail:

Somewhat confusingly, exceptions thrown from tasks make it to the uncaught exception handler only for tasks submitted with execute; for tasks submitted with submit, any thrown exception, checked or not, is considered to be part of the task’s return status. If a task submitted with submit terminates with an exception, it is rethrown by Future.get, wrapped in an ExecutionException.

The takeaway from this is an important one – never ignore any method that returns a Future! Ignoring Futures essentially acts to suppress any errors thrown from the code that completes the Future. If you use Error Prone, it will actually warn you if you accidentally forget.

So the question then becomes, what do I do with the Future? Often, the task was a Runnable, so I’m not interested in the return value. Perhaps I’d want to keep a reference around to cancel it at shutdown, but usually it’s easier just to shut down the whole executor service.

This is where you run into a wall with the plain Future API. Enter …

ListenableFutures

public interface ListenableFuture<V> extends Future<V>
{
    void addListener(Runnable listener, Executor executor);
}

The ListenableFuture API looks simple on the outside, but this one method affords an incredible amount of flexibility. It may not be obvious right now, but ListenableFuture can even provide us an easy way to handle errors that occur in our background tasks! Let’s explore some different use cases.

Creation

The easiest and most straightforward way to obtain a ListenableFuture is to configure your application’s ExecutorService as a ListeningExecutorService. For instance, if you are already using a cached thread pool:

ExecutorService executor = Executors.newCachedThreadPool();

Wrapping the creation in one method will leave you with a ListeningExecutorService:

ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());

This approach allows you to maintain your executor configuration, but now your executor will produce ListenableFutures instead of Futures. Pretty neat!

From Plain Futures

If you’re dealing with a third-party library or some code you can’t change that hands you a plain Future, your options are less ideal. Guava does provide the JdkFutureAdapters utilities, but these methods introduce complexities of their own that are hard to manage. You can read more in the Javadocs, but to make our (and our clients’) lives easier the best thing to do is to always return ListenableFutures directly.

Chaining Results

When you look at the ListenableFuture API, the use-case that most immediately comes to mind is chaining the result from one computation to another. The addListener method makes this extremely easy to do, while giving you full control over the executor that will run your chained task.

Imagine a simple Swing application where you want to dispatch a download operation in a background task. As soon as that task completes, you want to update the UI to display the image. Pulling this off with ListenableFutures can be done with two lines of code:

private void onImageDownloadButtonClicked()
{
    ListenableFuture<?> imageDownloadTask = executor.submit(this::downloadImage);
    imageDownloadTask.addListener(this::displayImage, SwingUtilities::invokeLater);
}

Notice that the download task is submitted to a background thread, but the chained UI update task gets handed to the UI thread using SwingUtilities. This keeps our UI responsive and offloads most of the complex chaining logic to the library.

Error Handling

While addListener can be handy in some situations, most commonly my needs are more involved. Maybe I’m just interested in making sure failures from background tasks don’t get swallowed up, or maybe I want control over both the failure and success handling for my tasks. This is where the true power of ListenableFuture shines through – in the many utilities provided in the Futures class.

The method I use the most with ListenableFutures is Futures.addCallback. Instead of providing a plain Runnable to be executed upon completion, this method allows you to specify a FutureCallback that can handle the success and failure cases separately.

public interface FutureCallback<V>
{
    void onSuccess(@Nullable V result);

    void onFailure(Throwable t);
}

This setup is perfect for the scenario we began with – ensuring that exceptions thrown in background tasks make their way to the surface and get handled. You can leave the onSuccess method empty and add simple logging to onFailure:

private void onSendButtonPressed()
{
    Futures.addCallback(
            executor.submit(this::sendData),
            new FutureCallback<Object>()
            {
                @Override
                public void onSuccess(@Nullable Object result)
                {
                }

                @Override
                public void onFailure(@NonNull Throwable t)
                {
                    logger.error(t);
                }
            },
            executor
    );
}

Notice that I’m specifying the same executor used to execute the background "send data" task as the one to execute our simple callback.

One thing to watch out for when creating FutureCallbacks is the case of cancelled tasks. Whether it’s in response to a user action (using Future.cancel()) or on application shutdown (using ExecutorService.shutdownNow()), cancellation will make its way to the onFailure() method of your FutureCallback in the form of a CancellationException.

Since this is typically expected and not a true "failure" case, I’ll usually check the error type to make sure it’s not a CancellationException before performing any error handling.

@Override
public void onFailure(@NonNull Throwable t)
{
    if (!(t instanceof CancellationException))
    {
        logger.error(t);
    }
}

API Considerations

Similar to my advice on using Immutable Collections, the best way to manage ListenableFutures from an API perspective (fields/return types) is to always refer to them as ListenableFutures instead of plain Futures. The addition of the one additional method opens up so much functionality for your clients, including all the utilities in the Futures class.

The difficulties of converting a plain Future to a ListenableFuture only emphasize the importance of returning ListenableFutures from the start. A ListeningExecutorService will hand you ListenableFutures anyways, so it’s best to keep them as such instead of trying to change it later when someone needs the extra method.

Jdk Alternatives

I’m sure many of you are screaming at this point – "what about CompletableFutures?!?" Added in Java 8, the CompletableFuture API supports completion listeners very similarly to ListenableFuture. It has a vast API and can support a variety of operations, but in my experience I’ve found the API to be a little "too vast". I’ve never come across a case where the ListenableFuture abstraction wasn’t sufficient, and I don’t like how CompletableFuture exposes a public complete method to clients. Ultimately, I prefer the simplicity and elegance of ListenableFuture.

Conclusion

In summary, always use ListeningExecutorService and ListenableFuture over ExecutorService and Future. You give your clients a tremendous amount of power for a small amount of effort. And for all your scheduled task needs, there’s also ListeningScheduledExecutorService and ListenableScheduledFuture!

Leave a Reply