CompletableFuture practical guide

Yurko
5 min readNov 9, 2017

--

Why

Java 9 is already here so it’s not perfect timing to write about Java 8 features but looking into current CompletableFuture blogs I can’t find all the information I needed when I had to work with it on daily basis. So perhaps to ease searching in the future I will try to write practical guide to CompletableFuture.

Some theory first:

CompletableFuture<T> is a class in java.util.concurrent package which implements both Future<T> and CompletionStage<T> interfaces. A Future represents the pending result of an asynchronous computation. And CompletionStage is a promise. Promise that computation will be done eventually. Having CompletionStage in place allows to chain the method calls with each other which creates very powerful mechanism for developers to code in asynchronous way. When working with CF, if you don’t specify a specific Executor, it will be submitted to ForkJoinPool.commonPool().

supplyAsync

Let’s start with the most basic example:

String computeSomething(){return "something"};
CompletableFuture<String> result = CompletableFuture.supplyAsync(this::computeSomething);

That’s it, supplyAsync takes a Supplier<T> and returns a completable future which will execute the supplier at some point, in this case it’s method reference to computeSomething method.

Chaining CF outputs

thenRun

Let’s try to chain some calls and check what is the difference between different available methods. Let’s assume we need to simply run some action without caring about the result:

CompletableFuture<String> result = CompletableFuture.supplyAsync(this::computeSomething);
CompletableFuture<Void> runResult = result.thenRun(
() -> System.out.println("Computation finished."))

thenRun method accepts a Runnable action and returns CompletableFuture<Void>.This way it can be used to chain unrelated events, where we don’t care about the actual return of previous CompletableFuture<T> call.

thenCompose

This method can be used when there is a need to call different methods that return CF and combine the result.

Let’s create few methods that we will use to play around with different types:

public CompletableFuture<String> computeSomething(){
return CompletableFuture.supplyAsync(()->"test");
}

public CompletableFuture<Integer> computeInteger(){
return CompletableFuture.supplyAsync(()->42);
}
public CompletableFuture<Boolean> computeBoolean(){
return CompletableFuture.supplyAsync(()->false);
}
public CompletableFuture<Boolean> computeBoolean(Integer i){
return CompletableFuture.supplyAsync(()->i > 100000);
}
public CompletableFuture<Integer> computeInteger(int i){
return CompletableFuture.supplyAsync(()->42+i);
}

All these methods will make asynchronous calls and will return CompletableFuture of different types. ThenCompose method allows to chain different methods and will return the last call:

CompletableFuture<Integer> runResult = computeSomething().thenCompose(s-> {
return computeInteger();
});
CompletableFuture<Boolean> booleanResult =
computeSomething()
.thenCompose(s-> computeInteger())
.thenCompose(i-> computeInteger(i))
.thenCompose(b->computeBoolean(b));

It is useful in chain pattern where you required to make lots of different calls to gather data but are interested only in the actual result of the last call.

thenApply

Method is used for working with a result of previous call, but the difference is that the return type will be combined of the all calls. For example if one chain completableFuture and simple method with thenApply then the result will be ComplateableFuture<MethodReturnType>. So this method is usually suitable to apply when there is one CF call and there is need to transform (enrich, change, filter etc) the result to specific needs:

CompletableFuture<String> runResult =   computeSomething().thenApply(s-> s + "test");CompletableFuture<Integer> runResultInt = computeSomething().thenApply(String::length);

Sometimes it is not the best practice is to use this method to chain different CF calls, as it will require to create same chain again to get to the actual result of chained methods:

CompletableFuture<String> computeSomething(String s){
return CompletableFuture.supplyAsync(()->"test" + s);
}
CompletableFuture<CompletableFuture<String>> runResultSomething = computeSomething().thenApply(this::computeSomething);

If it looks more or less ok for chain call of 2 methods, adding few more will be really painful, so if the idea is to chain CF methods then it’s better to use thenCompose.

thenAccept

This method behaves similar to thenRun with the difference that it takes Consumer and not Runnable as parameter.

CompletableFuture<Void> runResult = computeSomething().thenAccept(System.out::println);

One more common usage of accept method is thenAcceptBoth which takes CompletionStage and BiConsumer as parameters. Basically it allows easily to combine different completableFutures:

CompletableFuture<Void> runResultBoth = computeSomething()
.thenAcceptBoth(computeInteger(), (s, i)->
System.out.println(s+i));

Here we combined two calls computeSomething and computeInteger and output the result.

Waiting for completion

allOf

There are situations when there is need to wait for all CompletableFuture calls to complete. For example setting up preconditions to execute some flow (activateUser, purchaseService, activateService etc). But as an output of the flow the only relevant information is to proceed with the calls when all CF where executed:

CompletableFuture<Void> all = CompletableFuture
.allOf(computeInteger(), computeBoolean(), computeSomething());

The similar behavior is anyOf with a difference that the method will return as soon as any of the provided completionStages (as parameters) will be completed. As an example can be to establish one resource in the pool and proceed with first available.

There are common misuses of these methods when processing Collection of CompletableFutures:

List<CompletableFuture<Integer>> futures =IntStream.of(1,2,3,4,5)
.mapToObj(this::computeInteger).collect(Collectors.toList());
CompletableFuture.allOf(futures.get(0), futures.get(1), futures.get(2));

Both allOf and anyOff methods use CompletableFuture<?>… cfs as input so it’s not a good idea to process Collections this way. Of course there is possibility to write own wrapper for it is workaround and all workarounds are evil.

exceptionally

The method is used to setup what to do when something unexpected happens. In a world where tons of calls are made between different services sometimes exceptions will happen and to handle exceptional behavior java API have provide a method:

CompletableFuture<Void> runResult = computeSomething()
.thenAccept(System.out::println)
.exceptionally(t -> {
throw new Exception("can not compute any thing", t);
});

join, get

These methods are used to get the result from CF stage. Usually it’s bad practice to call any of these methods as it will force the thread to wait for the result.

String s = computeSomething().join();try {
String getString = computeSomething().get();
} catch (InterruptedException | ExecutionException e) {
//doSomething with exception
}

The difference between join and get is that join returns the result when completed or throws unchecked exception if completed exceptionally while get waits if necessary for the future to complete and if future is not completed with specified time (can be passed as parameter to the method) throws exception.

With new Stream API it’s really easy to use join with a List of CF

List<CompletableFuture<Integer>> futures =IntStream.of(1,2,3,4,5)
.mapToObj(this::computeInteger).collect(Collectors.toList());
List<Integer> intResults = futures.stream().map(CompletableFuture::join)
.collect(Collectors.toList());

Testing

Developing is always fun and. As part of development there are tests and when there are tests, there usually are mocks. Mocking completableFuture calls are quite simple. Assume we have service call like:

public interface CFService {  
CompletableFuture<String> doString();
}

And in test there is need to mock call to that service (for simplicity Mockito will be used) :

CFService cfService= mock(CFService.class);
when(cfService.doString()
.thenReturn(CompletableFuture.completedFuture("String"));

If one needs to mock exceptional flow it can be done in next way:

CFService cfService= mock(CFService.class);CompletableFuture<String> failFlow = new CompletableFuture<>();
c.completeExceptionally(new RuntimeException("failed"));
when(cfService.doString()
.thenReturn(failFlow);

In such way we tell the mock that cfService.doString() will be completed exceptionally and “.exceptionally()” flow will be executed.

Bad practices

While CF is a powerful mechanism provided by the Java API they should be used thoughtfully (like almost everything else in this fragile world but still). I will try few don’ts so it will be easier to remember:

  1. Don’t chain a lot of calls. While it is simple and fun to chain multiple calls supporting and testing them are quite of a pain
  2. get and join should be used with extra caution. These can block the current thread waiting for the CF to complete.
  3. Think whether implementation will really benefit from CF or if it is just quicker to make sync calls to provide a result

I hope this post will help at least someone so there will be more happy people in the world. Cheers :)

--

--

Yurko

Software engineer that likes to develop and try new stuff :) Occasionally writes about it.