Futures are better than callbacks

Published on 2016-08-14
Tagged: android concurrency java

View All Posts

A few weeks ago, I reviewed a large change in a legacy class shared by the Android apps I work on. This class is responsible for saving and loading URLs to and from local files. Sounds simple, right? Unfortunately, not.

This class was ridiculously large and complicated because of the way it implemented its asynchronous methods. Asynchrony is not bad per se, and it makes sense for long-running operations (like loading and saving files over the network). This class was ugly because it used several custom callback interfaces to notify clients when an operation was complete.

Here's a minimal example of how these callback interfaces were declared and used:

class FileLoader {
  // Callback interface to be implemented by clients.
  interface LoadCallback {
    void loadSucceeded(byte[] data);
    void loadFailed(Throwable error);
  }

  // Returns immediately. Calls the callback on completion. 
  public void loadUrl(String url, LoadCallback callback) {
    ...
  }

  ...
}

Callbacks are cumbersome for a number of reasons I'll get into below. Futures are better in pretty much every situation I can think of.

Why callbacks suck

Callbacks are worse than futures for several reasons. They are hard to use. Callers must declare a local class that implements a non-standard interface. This is annoying and can reduce the understandability of your code if you're calling multiple asynchronous methods on different classes with different callback interfaces (which is unfortunately common).

Callbacks are hard to implement. This file loader class kept a table of callbacks for ongoing operations. Since the class's methods could be called from multiple threads, this table had to be appropriately locked, so there's the additional risk of concurrency bugs.

When callbacks are used, it is easy to confuse what code gets executed on what thread. In Android (or any other interactive system), some operations must be done on the UI thread, but you need to be careful not to execute long-running code on the UI thread, because it can make the app stutter or hang. If you're implementing an I/O class with a dedicated worker thread, you also need to be careful not to execute client code that could block your I/O tasks. It's never really clear what thread a callback should run on unless an Executor is provided with the callback. If a direct executor is provided though, the callback may block the current thread. To work around this, both the file loader class and its clients posted tasks on background threads, resulting in a lot of redundant grungy concurrency code.

One last problem is that callbacks are hard to compose. If you need to call one asynchronous method after another, or if you need some code to run when several parallel asynchronous calls have completed, callbacks will make your life difficult. Since callback interfaces are all different, you'll need to hack something together every time you do this.

Why futures are better

A Future<T> represents an asynchronous operation that will return a result of type T. A promise is a closely related concept, and the words are often used interchangeably. Futures have several advantages over callbacks.

Futures have a standard interface. To wait for the result of a future, just call get(). It will block and return the result, or it will throw an ExecutionException if there was an error. You can also call cancel() to cancel or interrupt a task. If you need something that actually behaves like a callback, you can use FutureCallback (from Guava). Either way, if you have a code base built around futures, then asynchronous operations will be handled in a standard way, and your code will be more readable.

Futures make asynchronous methods easier to implement. You no longer need to track callbacks for ongoing operations. Your methods can just return a Future<T> instead of saving and tracking a callback. You can obtain a  Future<T> by calling ExecutorService.submit() on a thread pool with a Callable<T> that does the synchronous work. SettableFuture (Guava) and CompletableFuture (Java 8) are available if you need more manual control.

Finally, futures are composable. If you're using Guava, the Futures class provides several methods for this. Futures.transform() and transformAsync() let you run some (possibly asynchronous) code after the future completes, producing a new future. These methods both accept an optional Executor argument that lets you dictate what thread your code will run on. allAsList() is useful for combining several futures into a single future (which can then be appropriately chained and transformed). If you're using Java 8, you may also want to check out CompletableFuture, which has several useful composition methods built-in. It's not available on older Android devices though.

Thanks!

I want to thank my co-worker Jason, who was the author of the change I mentioned at the beginning. Adding new functionality to legacy code is rarely fun. He showed me a short design doc a few days later for refactoring this class to use futures. I think it will be a lot simpler.