CompletableFuture thenApply vs thenCompose

CompletableFuture thenApply vs thenCompose

1. Overview

In this article, we will learn the differences between thenApply vs thenCompose methods of the CompletableFuture. To learn about other methods available in the CompletableFuture, refer to these articles.

2. CompletableFuture thenApply vs thenCompose

You can process and transform the result of the CompletableFuture by using the thenApply and thenCompose methods.

The major difference lies in the return type of these methods. The thenApply function can return non-future types (like primitives, Object) as well as nested Future as output while thenCompose flattens the structure and returns a CompletableStage directly as the result. The CompletionStage<T> is an interface of which CompletableFuture<T> is the only current implementing class.

For example, the thenApply returns as a nested future while thenCompose flattens the structure returns the Future directly without nesting.

You can compare thenApply to map and thenCompose to flatMap.

CompletableFuture<CompletableFuture<Integer>> thenApplyFuture =
    numberFuture.thenApply(number -> { 
      return calculateSquare(number)
});
CompletableFuture<Integer> thenComposeFuture =
    numberFuture.thenCompose(number -> { 
      return calculateSquare(number)
});

Let’s see this difference with examples.

2.1. thenApply of CompletableFuture

The thenApply method accepts a Function as an argument. The Function receives the result of the CompletableFuture and then process it. You can write code that needs to be executed synchronously inside this function.

As discussed earlier, the thenApply can return non-future types as output.

Assume we have a CompletableFuture that returns a natural number as result after performing some computations.

 CompletableFuture<Integer> numberCompletableFuture = CompletableFuture.supplyAsync(() -> {
      // performs some computations
      System.out.println("numberCompletableFuture executes in : " + Thread.currentThread().getName());
      return 5;
 });

Now, the following code in thenApply function gets the result (natural number) from the numberCompletableFuture and calculates square of it. This thenApply waits for the numberCompletableFuture to complete its execution. Then immediately calculates the square and returns the Integer as value.

CompletableFuture<Integer> squareNumber = numberCompletableFuture.thenApply(number -> number * number);

If you execute the above code, then the following statements appear in the console:

numberCompletableFuture executes in : ForkJoinPool.commonPool-worker-3
squareNumberFuture executes in : ForkJoinPool.commonPool-worker-3

The thenApply can also return nested future.

CompletableFuture<CompletableFuture<Integer>> squareNumberFuture = numberCompletableFuture.thenApply(number -> {
            System.out.println("thenCompose executes in : " + Thread.currentThread().getName() +
                    Thread.currentThread().isDaemon());
            return calculateSquare(number);
        });
private CompletableFuture<Integer> calculateSquare(Integer number) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("squareNumberFuture executes in : " + Thread.currentThread().getName());
            return number * number;
        });
    }

2.2. thenCompose of CompletableFuture

You can perform asynchronous processing by using the thenCompose which flattens the structure and returns a CompletableFuture. Note that this can return only CompletableStage as the result. The CompletionStage<T> is an interface of the implementation class CompletableFuture<T>.

Let’s rewrite the square code by using the thenCompose method. Note that the code inside the CompletableFuture returned by the thenCompose executes asynchronously.

CompletableFuture<Number> squareNumberFuture = numberCompletableFuture.thenCompose(number -> {
     System.out.println("thenCompose executes in : " + Thread.currentThread().getName());
     return CompletableFuture.supplyAsync(() -> {
          try {
               Thread.sleep(5000);
          } catch (InterruptedException e) {
               e.printStackTrace();
          }
          System.out.println("squareNumberFuture executes in : " + Thread.currentThread().getName());
          return number * number
    });
});

If you execute the above code, then the following statements are printed in the console:

numberCompletableFuture executes in : ForkJoinPool.commonPool-worker-3
thenCompose executes in : ForkJoinPool.commonPool-worker-3
squareNumberFuture executes in : ForkJoinPool.commonPool-worker-5

3. Conclusion

To sum up, we have learned the difference between the thenApply and thenCompose methods of the CompletableFuture with relevant examples.

Leave a Reply

Your email address will not be published. Required fields are marked *