October 16, 2022

Java CompletableFuture With Examples

In this post we’ll learn about CompletableFuture class in Java along with examples to understand the functionalities this class provides.

CompletableFuture in Java

CompletableFuture is used for asynchronous computation of the task where the task is executed by a separate thread and the result is returned when it is ready.

How is CompletableFuture different from Future

You must be wondering there is already a Future interface doing the same job of asynchronous computation and returning a value then what does Java CompletableFuture has to offer.

Future interface doesn’t provide a lot of features, in fact to get a result of asynchronous computation there is only future.get() method which is blocking so there is no scope for running multiple dependent tasks in a non-blocking fashion.

That is where CompletableFuture with its rich API shines. It provides functionality to chain multiple dependent tasks which can be run asynchronously. So you can create a chain of tasks where the next task is triggered when the result of the current task is available.

For example-
CompletableFuture.supplyAsync(()->{return 4;})
.thenApplyAsync(num-> Math.pow(num, 2))
.thenAccept(num-> System.out.println("Value- " + num))
.thenRun(()->System.out.println("Done"));

Here first task returns a value, once the value is available next task does its computation and then the next task in the chain is executed.

Another advantage of Java CompletableFuture is that it provides method to handle exceptions thrown in any of the dependent stages.

CompletableFuture class in Java implements Future and CompletionStage interfaces. CompletableFuture class gets its behavior of running tasks as dependent stages by implementing CompletionStage interface.

Important points about Java CompletableFuture

  1. CompletableFuture can be used as a Future that is explicitly completed or it can be used as a CompletionStage where completion of one stage triggers another dependent stage.
  2. CompletableFuture provides both async and non-async variants of a method.
  3. In case of a async method you can provide an Executor as an argument, in that case thread from the thread pool created using Executor is used for executing tasks. When async method without an Executor argument is used then the thread from the ForkJoinPool.commonPool() is used to execute tasks.

    For example consider the following three variants of thenApply() method-

    • thenApply(Function<? super T,? extends U> fn)- This one is non-async method.
    • thenApplyAsync(Function<? super T,? extends U> fn)- Asynchronous version, since executor is not passed as an argument so uses the default asynchronous execution facility (ForkJoinPool.commonPool()).
    • thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)- Another asynchronous variant of thenApply() method, executed using the supplied Executor.

CompletableFuture Java examples

1- Simple example where a CompletableFuture instance is created using its constructor and the explicitly completed using the complete() method.

static void cfExample() throws InterruptedException, ExecutionException {
  CompletableFuture<String> cf = new CompletableFuture<>();
  cf.complete("CompletableFuture completed");
  System.out.println("Value- " + cf.get());
}
Output
Value- CompletableFuture completed

2- Using runAsync() method to execute an asynchronous task which returns a CompletableFuture<Void>.

static void cfExample() throws InterruptedException, ExecutionException {
  CompletableFuture<Void> cf = CompletableFuture.runAsync(()->{
    System.out.println("Running a runnable task");
  });
  System.out.println("Returned Value- " + cf.get());
}
Output
Running a runnable task
Returned Value- null

3- As you can see from the previous example runAsync() method doesn’t return a result. If you want value to be returned then you can use supplyAsync() method.

static void cfExample() throws InterruptedException, ExecutionException {
  CompletableFuture<String> cf = CompletableFuture.supplyAsync(()->{
    System.out.println("Running a task");
    return "Task Completed";
  });

  System.out.println("Returned Value- " + cf.get());
}
Output
Running a task
Returned Value- Task Completed

4- Till now we have seen examples with only one method, now let’s see some examples where chain of tasks is executed.

static void cfExample() throws InterruptedException, ExecutionException {
  StringBuilder sb = new StringBuilder();
  CompletableFuture<String> cf = CompletableFuture.supplyAsync(()->{
    return "Completable";
  }).thenApply(s->sb.append(s).append("Future").toString());
  System.out.println("Returned Value- " + cf.get());
}
Output
Returned Value- CompletableFuture

In the example there are two stages-

  1. In first stage supplyAsync() method is executed which returns a result. When this stage completes normally it triggers the next stage.
  2. When the first stage completes, its result is then applied to the aptly named method thenApply().
  3. Since thenApply() method is used, which is non-async, so it will be executed by the same thread that executes the supplyAsync() method, it may also be executed by a thread that calls the supplyAsync() method (main thread).

5- Using the asynchronous variant of the method.

static void cfExample() throws InterruptedException, ExecutionException {
  StringBuilder sb = new StringBuilder();
  CompletableFuture<String> cf = CompletableFuture.supplyAsync(()->{
    return "Completable";
  }).thenApplyAsync(s->sb.append(s).append("Future").toString());

  System.out.println("Returned Value- " + cf.get());
}

It is same as the previous example, only difference is that it uses the async variant of the thenApply() method i.e. thenApplyAsync(). Now the chained task will be executed asynchronously using a separate thread obtained from ForkJoinPool.commonPool().

6- You can supply an Executor with the asynchronous variant of the method.

static void cfExample() throws InterruptedException, ExecutionException {
  ExecutorService executor = Executors.newFixedThreadPool(2);
  StringBuilder sb = new StringBuilder();
  CompletableFuture<String> cf = CompletableFuture.supplyAsync(()->{
    return "Completable";
  }).thenApplyAsync(s->sb.append(s).append("Future").toString(), executor);

  System.out.println("Returned Value- " + cf.get());
  executor.shutdown();
}

Now the chained task will be executed asynchronously using the passed executor and uses the separate thread obtained from the fixed thread pool.

7- If you just want to consume the result of the previous stage with out returning any result you can use thenAccept() or thenRun() methods of the CompletableFuture class.

In thenAccept method Consumer (a functional interface) is passed as parameter and it returns CompletionStage<Void>.

In thenRun() method Runnable is passed as parameter and it returns CompletionStage<Void>.

Though thenAccept() method can access the result of the task completed before it, thenRun() method doesn’t have access to the result of the task completed before it.

static void cfExample() throws InterruptedException, ExecutionException {
  StringBuilder sb = new StringBuilder();
  CompletableFuture.supplyAsync(()->{return "Completable";})
    .thenApplyAsync(s->sb.append(s).append("Future").toString())
    .thenAccept(s->System.out.println("Current value is- " + s));
}
Output
Current value is- CompletableFuture
Using thenRun()
static void cfExample() throws InterruptedException, ExecutionException {
  StringBuilder sb = new StringBuilder();
  CompletableFuture.supplyAsync(()->{return "Completable";})
    .thenApplyAsync(s->sb.append(s).append("Future").toString())
    .thenRun(()->System.out.println("Process completed"));
}

Using Java CompletableFuture’s thenCompose() method

In CompletableFuture class there is another method thenCompose() where the computation performed by a stage can be expressed as a Function, another method which does the same is thenApply(). How these two methods thenCompose() and thenApply() differ is the how the value is returned.

  • thenApply() method returns a new CompletionStage with a type determined by the computation.
  • thenCompose() method returns a new CompletionStage with a type similar to the previous stage.

Let’s try to clarify it with an example. Here we have two methods getValue() and getAnotherValue() both returning CompletableFuture<String>. First we’ll use thenApply() method.

static void cfExample() throws InterruptedException, ExecutionException {
  CompletableFuture<CompletableFuture<String>> cf = getValue().thenApply(s->getAnotherValue(s));
  System.out.println("Value- " + cf.get().get());
}

static CompletableFuture<String> getValue(){
  return CompletableFuture.supplyAsync(()->{return "Completable";});
}

static CompletableFuture<String> getAnotherValue(String str){
  return CompletableFuture.supplyAsync(()->{return str+"Future";});
}

If you see the chain here, there is a getValue() method which returns CompletableFuture<String> which is then used in the thenApply() method which again returns a result of type CompletableFuture<String> making it a nested structure of CompletableFuture<CompletableFuture<String>>.

When you use thenCompose() method returned result has a type similar to the previous stage. That helps in flattening the nested structure.

static void cfExample() throws InterruptedException, ExecutionException {
  CompletableFuture<String> cf = getValue().thenCompose(s->getAnotherValue(s));
  System.out.println("Value- " + cf.get());
}

static CompletableFuture<String> getValue(){
  return CompletableFuture.supplyAsync(()->{return "Completable";});
}

static CompletableFuture<String> getAnotherValue(String str){
  return CompletableFuture.supplyAsync(()->{return str+"Future";});
}

Java CompletableFuture - Operations with more than one Completion stage

1- Combining the result of two completion stages- You can combine two independent Completion stages using thenCombine() method which is executed with the results of two Completion stages as arguments to the supplied function.

Here we have two methods getValue() and getAnotherValue() both returning CompletableFuture<String>. Once both of these Completion stages are completed, thenCombine() method is called with the results of both.

static void cfExample() throws InterruptedException, ExecutionException {
  CompletableFuture<String> cf = getValue().thenCombine(getAnotherValue(), (s1, s2)->s1+ " " +s2);
  System.out.println("Value- " + cf.get());
}

static CompletableFuture<String> getValue(){
  return CompletableFuture.supplyAsync(()->{return "Hello";});
}

static CompletableFuture<String> getAnotherValue(){
  return CompletableFuture.supplyAsync(()->{return "World";});
}
Output
Value- Hello World

2- Consuming the result of two completion stages- Just like thenAccept() method in Java CompletableFuture consumes the result of a completion stage there is also a thenAcceptBoth() method which consumes the result of two completion stages.

static void cfExample() throws InterruptedException, ExecutionException {
  CompletableFuture<Void> cf = getValue().thenAcceptBoth(getAnotherValue(), 
       (s1, s2)->System.out.println("Process completed with results- " +s1+ " " +s2));
  //System.out.println("Value- " + cf.get());
}

static CompletableFuture<String> getValue(){
  return CompletableFuture.supplyAsync(()->{return "Hello";});
}

static CompletableFuture<String> getAnotherValue(){
  return CompletableFuture.supplyAsync(()->{return "World";});
}
Output
Process completed with results- Hello World

3- Applying either of the two- If there are two CompletableFutures and only one of the stage completes normally and you want to apply the function on the result of that completion stage that completes normally then you can use applyToEither() method.

In the example there are two methods getValue() and getAnotherValue(). In the getValue() method it is made to throw an exception and the method completes exceptionally. On the other hand getAnotherValue() method completes normally.

static void cfExample() throws InterruptedException, ExecutionException {
  CompletableFuture<String> cf = getValue().applyToEitherAsync(getAnotherValue(), (s)->s.toUpperCase());
  System.out.println("Value- " + cf.get());
}

static CompletableFuture<String> getValue(){
  String str = null;
  return CompletableFuture.supplyAsync(() -> {
    if (str == null) {
      throw new IllegalArgumentException("Invalid String passed  " + str);
    }
    return str;
  }).exceptionally(exp -> {
    System.out.println("Exception message- " + exp.getMessage());
    return "";
  });
}

static CompletableFuture<String> getAnotherValue(){
  return CompletableFuture.supplyAsync(()->{return "World";});
}
Output
Exception message-  java.lang.IllegalArgumentException: Invalid String passed null
Value- WORLD

As you can see applyToEitherAsync() method uses the result of the completion stage which completes normally.

Exception handling in Java CompletableFuture

For exception handling in Java CompletableFuture there are three methods-

  • handle
  • whenComplete
  • exceptionally

handle and whenComplete methods are always executed whether an exception is thrown in the triggering stage or stage completes normally.

Exceptionally method is executed only when the triggering stage completes exceptionally.

Java CompletableFuture - Exception handling using exceptionally

In the example String is passed as null causing an exception which results in exceptionally being called.

static void cfExample() throws InterruptedException, ExecutionException {
  String str = null;
  CompletableFuture.supplyAsync(() -> {
    if (str == null) {
      throw new IllegalArgumentException("Invalid String passed " + str);
    }
    return str;
  }).exceptionally(exp -> {
      System.out.println("Exception message- " + exp.getMessage());
      return "";
  });
}
Output
Exception message- java.lang.IllegalArgumentException: Invalid String passed null

If there is no exception in the triggering stage exceptionally won’t be called.

static void cfExample() throws InterruptedException, ExecutionException {
  String str = "Hello";
  CompletableFuture<String>cf = CompletableFuture.supplyAsync(() -> {
    if (str == null) {
      throw new IllegalArgumentException("Invalid String passed " + str);
    }
    return str;
  }).exceptionally(exp -> {
    System.out.println("Exception message- " + exp.getMessage());
    return "";
  });
  System.out.println("Value- " + cf.get());
}
Output
Value- Hello

Java CompletableFuture - Exception handling using handle

handle() method is executed with this stage's result and exception as arguments to the supplied function. If no exception is thrown then exception argument would be null. Note that handle method is always executed whether exception is thrown or not, by checking exception argument for null it can be determined exception handling code is to be executed or not.

static void cfExample() throws InterruptedException, ExecutionException {
  String str = null;
  CompletableFuture<String>cf = CompletableFuture.supplyAsync(() -> {
    if (str == null) {
      throw new IllegalArgumentException("Invalid String passed " + str);
    }
    return str;
  }).handle((s, e) -> {
    if(e != null) {
      System.out.println("Exception message- " + e.getMessage());
      s = "";
    }
    return s;
  });
  System.out.println("Value- " + cf.get());
}
Output
Exception message- java.lang.IllegalArgumentException: Invalid String passed null
Value-

Java CompletableFuture - Exception handling using whenComplete

Returns a new CompletionStage with the same result or exception as this stage so result can't be changed in the whenComplete method. Note that whenComplete method is always executed whether exception is thrown or not, by checking exception argument for null it can be determined exception handling code is to be executed or not.

static void cfExample() throws InterruptedException, ExecutionException {
  String str = "Hello";
  CompletableFuture<String>cf = CompletableFuture.supplyAsync(() -> {
    if (str == null) {
      throw new IllegalArgumentException("Invalid String passed " + str);
    }
    return str;
  }).whenComplete((s, e) -> {
    System.out.println("In when complete method");
    if(e != null) {
      System.out.println("Exception message- " + e.getMessage());
    }
  });
  System.out.println("Value- " + cf.get());
}
Output
In when complete method
Value- Hello

As you can see in the example exception is not thrown in the stage still whenComplete method is invoked but exception argument would be null in this case.

That's all for the topic Java CompletableFuture With Examples. If something is missing or you have something to share about the topic please write a comment.


You may also like

No comments:

Post a Comment