CompletableFuture in Java Simplified
By: Antariksh Singh
Tags: Completablefuture, Asynchronous Programming, Parallel Computing, Tutorial, Java

Photo by Fotis Fotopoulos on Unsplash

1. Introduction

With the introduction of CompletableFuture in Java 8, the world of asynchronouse programming took a massive step forward. But one may wonder the real reason behind its introduction when we already had Future interface since Java 5. The real motivation behind CompletableFuture was to overcome certain limitations of Future interface:

1. There was no way to make get() call non-blocking in Future.

2. No support for exception handling.

3. Chaining of callbacks was not supported.

4. There was no way to explicitly complete the task.

In this article, we will see some common usage of CompletableFuture .

2. Creating a CompletableFuture Task

First of all to create a new completableFuture task, simply call the constructor

CompletableFuture<String> task = new CompletableFuture<String>();

Also, you can explicitly create a completed task

CompletableFuture<String> completedTask = CompletableFuture.completedFuture("Some Value");

Or mark the task as completed by calling the complete method. All the clients waiting for task to complete will get the specified result.

task.complete("Task completed forcefully");

To get the result of this CompletableFuture we can call get() method.

if(completedTask.isDone()){

completedTask.get();

}

3. Running a task in ‘fire and forget’ mode

In some scenario we might need to fire the function and the result of the function call is not used in the current function. This can be achieved using CompletableFuture’s runAsync .

CompletableFuture.runAsync(()-> doSomething());

Well, that’s how simple it is to trigger a task in a new thread. But you might be thinking who is actually executing the task when nobody assigned a new thread. CompletableFuture executes these tasks in a thread obtained from the global ForkJoinPool.commonPool().

But it also provides the user with an option to separately assign a new thread from the user defined Thread pool and pass it to the method.

Executor executor = Executors.newFixedThreadPool(5);

CompletableFuture.runAsync(()-> doSomething(), executor);

4. Run an asynchronous task and return the result

Now what if you want the result from the background task and do some transformations on it, one simply can do it using supplyAsync method

CompletableFuture<String> future =

CompletableFuture.supplyAsync(() -> doSomething());

Similar to runAsync , supplyAsync supports the option to pass an executor with which the task will be executed in a thread obtained from the Executor’s thread pool.

5. CompletableFuture Callbacks

As discussed earlier, .get() will return the result set of the future task. But there’s a catch, this is a blocking call. But that’s not what we want, right?

Enter Callbacks. CompletableFuture gives you a way to attach a callback which will get called when the task completes, that way we can write the transformation logic that will execute after the completion of the task.

CompletableFuture provides you thenApply(), thenAccept() and thenRun() methods to attach to the task.

thenApply

CompletableFuture<String> version =

CompletableFuture.supplyAsync(() -> "8")

.thenApply(v -> { return "Java " + v; });

Pretty simple, right? But wait, thenApply task is performed in the same thread as supplyAsync task. What if we want to run the thenApply task in a separate thread and this is possible using thenApplyAsync callback, it will be executed in a thread obtained from ForkJoinPool.commonPool() , alternatively you can specify an executor.

CompletableFuture<String> programmingLanguage =

CompletableFuture.supplyAsync(() -> "8");

.thenApplyAsync(v -> { return "Java " + v; });

thenAccept

After the completion of the task, if we want to run some piece of code and not return anything from our callback method, thenAccept and thenRun comes in handy in those case.

thenAccept takes a Consumer<T> and returns CompletableFuture<Void>

CompletableFuture.supplyAsync(() -> "8")

.thenAccept(v -> {

System.out.println("Java Version " +v); });

thenRun

While thenAccept has access to the result of the CompletableFuture, thenRun doesn’t. It takes a Runnable and returns CompletableFuture<Void>.

CompletableFuture.supplyAsync(this::createOrder)

.thenRun(()->{

// do something

});

Key Differences

7. Waiting for tasks to Complete

Now some tasks can run independently in background, but there might be a scenario where you want all or any of the tasks to be completed before you do something else. allOf or anyOf function does the same.

allOf

As the name suggests, allOf is used when you want a list of tasks to be completed before you move ahead.

List<CompletableFuture<String>> pdfFutures = htmls.stream()

.map(html -> convertToPdf(html))

.collect(Collectors.toList());





// Now if you want to do some operation when all the htmls are

// are converted to pdf, we can call allOf callback.

CompletableFuture.allOf(pdfFutures.toArray(

new CompletableFuture[pdfFutures.size()])

anyOf

anyOf is used when you’re waiting for any of the task to complete before moving ahead.

Usage is quite similar to what we have for allOf

List<CompletableFuture<String>> pdfFutures = htmls.stream()

.map(html -> convertToPdf(html))

.collect(Collectors.toList());





// anyOf is called when you want any of the task to complete.

CompletableFuture.anyOf(pdfFutures.toArray(

new CompletableFuture[pdfFutures.size()])

8. Exception Handling for CompletableFuture tasks

Now what if things go wrong and you just want the code to gracefully handle failure. CompletableFuture provides two callbacks, exceptionally and handle which give you the control of the exception and take action accordingly.

exceptionally

Similar to catch block exceptionally callback gives you chance to catch the exception and return a fallback value.

CompletableFuture<String> status =

CompletableFuture.supplyAsync(() -> doSomething())

.exceptionally(ex -> {

System.out.println("Something went wrong " + ex.getMessage());

return "FAILURE";

});

handle

CompletableFuture offers another callback to recover from exceptions. As the Java Doc suggests,

Returns a new CompletionStage that, when this stage completes either normally or exceptionally, is executed with this stage’s result and exception as arguments to the supplied function.

handle callback will be called whether or not exception occurred.

CompletableFuture<String> status =

CompletableFuture.supplyAsync(() -> doSomething())

.handle((res, ex) -> {

if(ex != null) {

System.out.println("Something went wrong " + ex.getMessage());

}

return res;

});

Conclusion

That’s all folks!