ListenableFuture vs CompletableFuture — a comparison

Pramod Biligiri
Pramod Biligiri’s Blog
2 min readNov 8, 2019

In my previous blog post I wrote about how Google Guava’s ListenableFuture is an improvement over Java 6’s Future class. But Java 8 ships with a CompletableFuture class that brings much of the same benefits into the standard Java API. The below code example (again with inline comments) shows how to use CompletableFuture to implement the same logic of first looking up a userId and then converting it into a username, both asynchronously

public class CompletableFutureExample1 {    public static void main(String[] args) throws ExecutionException, InterruptedException {
// Step 1: Create a regular thread pool based Executor
ExecutorService executor = Executors.newFixedThreadPool(5);
// Step 2: Provide a piece of logic that will be executed asynchronously within the above executor, and whose
// result is encapsulated within a CompletableFuture
CompletableFuture<UserId> userIdFuture = CompletableFuture.supplyAsync(() -> new UserId(1), executor);
// Step 3: Connect the output of the userIdFuture to logic that converts it into userDetails,
// again asynchronously
CompletableFuture<UserDetails> userDetailsFuture =
userIdFuture.thenApplyAsync(userId -> new UserDetails(userId), executor);
// Step 4: Once the userDetails is computed, print out the username, again asynchronously
CompletableFuture<Void> voidCompletableFuture =
userDetailsFuture.thenAcceptAsync(userDetails -> System.out.println(userDetails.getUsername()), executor);
// Step 5: Wait for the final step to complete:
voidCompletableFuture.get();
executor.shutdownNow();
}
private static class UserId {
private long id;
public UserId(long id) {
this.id = id;
}
}
private static class UserDetails {
private UserId userId;
public UserDetails(UserId userId) {
this.userId = userId;
}
public String getUsername() {
// this can be replaced by a lookup based on userId
return "some username";
}
}
}

Thanks to the “fluent” nature of the CompletableFuture methods, the above steps can be combined into a single statement as below, without losing any of the type safety:

CompletableFuture.
supplyAsync(() -> new UserId(1), executor).
thenApplyAsync(userId -> new UserDetails(userId), executor).
thenAcceptAsync(userDetails -> System.out.println(userDetails.getUsername()), executor).
get();

CompletableFuture is implemented in an interesting way, by combining the pre-existing Future interface with a newly introduced (in Java 8) CompletionStage interface. CompletionStage tries to accommodate all the ways in which two pieces of logic can be combined together: synchronously/asynchronously, where they each may be functions that take and/or return values. This leads to an explosion of methods in the interface, with names like: thenAccept, thenAcceptAsync, thenApply, thenApplyAsync, thenRun, thenRunAsync… and so on.

Doug Lea, the author of this piece of code in the JDK (and also of a bulk of Java’s concurrency classes) outlines the reasons for this in an email from 2016 (link). According to him, CompletableFuture implements the entire mammoth interface of CompletionStage, but you can create subclasses that disable certain features of the same, by just throwing UnsupportedOperationException appropriately. As of now, CompletableFuture is the only subclass of CompletionStage within the JDK itself!

CompletableFuture also has a few static methods like “anyOf”, “allOf” that check for the completion of a list of provided futures. Lastly, Java 8 also provides an ExecutorCompletionService that can be used to track the status of a list of submitted Futures and trigger an action when all of them succeed/fail etc.

By no means is this an exhaustive description of CompletableFuture. There are aspects of error handling, and explicit completion that are not covered in this blog post.

--

--