Future vs CompletableFuture in Java- #2

Reading Time: 3 minutes

In our previous blog – Future vs CompletableFuture – #1, we compared Java 5’s Future with Java 8’s CompletableFuture on the basis of two categories i.e. manual completion and attaching a callable method. Now, we will be comparing them on the basis of next 3 categories i.e.

  • Combining 2 CompletableFutures together
  • Combining multiple CompletableFutures together
  • Exception Handling

Let’s have a look at each one of them.

3. Combining 2 CompletableFutures together

In case of Future, there is no way to create asynchronous workflow i.e. long running computation. But CompletableFuture provides us with 2 methods to achieve this functionality:

i) thenCompose()

It is a method of combining 2 dependent futures together. This method takes a function that returns a CompletableFuture instance. The argument of this function is the result of the previous computation step. This allows us to use this value inside the next CompletableFuture‘s lambda.

For example:



This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


private static void thenCompose() {
CompletableFuture<String> completableFuture =
CompletableFuture.supplyAsync(() -> "Hello")
.thenCompose(value ->
CompletableFuture.supplyAsync(
() -> value + " Knolders! Its thenCompose"));
completableFuture.thenAccept(System.out::println); // Hello Knolders! Its thenCompose
}

Let’s compare this with the supplier method, thenApply() :



This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


private static void thenApply() {
CompletableFuture<CompletableFuture<String>> completableFuture =
CompletableFuture.supplyAsync(() -> "Hello")
.thenApply(value -> CompletableFuture.supplyAsync(
() -> value + " Knolders! Its thenApply"));
//Perform operation
}
view raw

thenApply.java

hosted with ❤ by GitHub

As you can see, the thenCompose() method is returning a value of type CompletableFuture whereas thenApply() is returning the value of type CompletableFuture in the same scenario.

Note: The thenCompose method together with thenApply implement basic building blocks of the monadic pattern. They closely relate to the map and flatMap methods of Stream and Optional classes also available in Java 8.

ii) thenCombine()

It is a method of combining 2 independent futures together and do something  with there result after both of them are complete. Combining is accomplished by taking 2 successful CompletionStages and having the results from both used as parameters to a BiFunction to produce another result.  For example:



This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


private static void thenCombine() {
CompletableFuture<String> completableFuture
= CompletableFuture.supplyAsync(() -> "Hello")
.thenCombine(CompletableFuture.supplyAsync(
() -> " Knolders! Its thenCombine"), (value1, value2) -> value1 + value2);
completableFuture.thenAccept(System.out::println); // Hello Knolders! Its thenCombine
}

iii) thenAcceptBoth()

It is used when you want to perform some operation with two independent Future’s result but don’t need to pass any resulting value down a Future chain. For example:



This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


private static void thenAcceptBoth() {
CompletableFuture<Void> completableFuture = CompletableFuture.supplyAsync(() -> "Hello")
.thenAcceptBoth(CompletableFuture.supplyAsync(() -> " Knolders! Its thenAcceptBoth"),
(value1, value2) -> System.out.println(value1 + value2)); // Hello Knolders! Its thenAcceptBoth
}

4. Combining multiple CompletableFutures together

What if there comes a scenario where you want to combine 100 different Futures that you want to run in parallel and then run some function after all of them completes. Future does not provide us any way in order to achieve this functionality but CompletableFuture does. There are two methods of implementing this:

i) CompletableFuture.allOf()

CompletableFuture.allOf()  static method is used in scenarios when you have a List of independent futures that you want to run in parallel and do something after all of them are complete.



This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


private static void allOf() {
CompletableFuture<String> completableFuture1
= CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> completableFuture2
= CompletableFuture.supplyAsync(() -> "Knolders!");
CompletableFuture<String> completableFuture3
= CompletableFuture.supplyAsync(() -> "Its allOf");
CompletableFuture<Void> combinedFuture
= CompletableFuture.allOf(completableFuture1, completableFuture2, completableFuture3);
}
view raw

allOf.java

hosted with ❤ by GitHub

Limitation:

The limitation of this method is that the return type is CompletableFuture i.e. it does not return the combined results of all Futures. Instead you have to manually get results from Futures.



This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


combinedFuture.get();
assertTrue(future1.isDone());
assertTrue(future2.isDone());
assertTrue(future3.isDone());
view raw

get.java

hosted with ❤ by GitHub

Fortunately, CompletableFuture.join() method and Java 8 Streams API helps to resolve this issue:



This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


combinedFuture.thenApply(v ->
Stream.of(completableFuture1, completableFuture2, completableFuture3).
map(CompletableFuture::join).
collect(Collectors.toList()));
String combined = Stream.of(completableFuture1, completableFuture2, completableFuture3)
.map(CompletableFuture::join)
.collect(Collectors.joining(" "));
System.out.println(combined); // Hello Knolders! Its allOf
view raw

gistfile1.txt

hosted with ❤ by GitHub

Note : 

The CompletableFuture.join() method is similar to the CompletableFuture.get() method, but it throws an unchecked exception in case the Future does not complete normally. This makes it possible to use it as a method reference in the Stream.map() method.

ii) CompletableFuture.anyOf()

CompletableFuture.anyOf() as the name suggests, returns a new CompletableFuture which is completed when any of the given CompletableFutures complete, with the same result. CompletableFuture.anyOf() takes a varargs of Futures and returns CompletableFuture.



This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


private static void anyOf() throws Exception {
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Result of Future 1";
});
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Result of Future 2";
});
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Result of Future 3";
});
CompletableFuture<Object> anyOfFuture = CompletableFuture.anyOf(future1, future2, future3);
anyOfFuture.thenAccept(System.out::println); // Result of Future 2
}
view raw

allOf.java

hosted with ❤ by GitHub

Limitation:The problem with CompletableFuture.anyOf() is that if you have CompletableFuture that return results of different types, then you won’t know the type of your final CompletableFuture.

5. Exception Handling

Let’s first understand how errors are propagated in a callback chain. Consider the following CompletableFuture callback chain –



This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


private static void chainOfThenApply() {
CompletableFuture.supplyAsync(() -> {
// Code which might throw an exception
return "Some result";
}).thenApply(result -> {
return "processed result";
}).thenApply(result -> {
return "result after further processing";
}).thenAccept(result -> {
// do something with the final result
});
}
view raw

chain.java

hosted with ❤ by GitHub

If an error occurs in the original supplyAsync() task, then none of the thenApply() callbacks will be called and future will be resolved with the exception occurred. If an error occurs in first thenApply() callback then 2nd and 3rd callbacks won’t be called and the future will be resolved with the exception occurred, and so on.

So, there are 2 ways in order to handle this scenario:

i) Handle exceptions using exceptionally() callback

exceptionally() gives us a chance to recover by returning a default value or taking an alternative function that will be executed if preceding calculation fails with an exception.



This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


private static void exception() {
Integer age = -1;
CompletableFuture<String> exceptionFuture = CompletableFuture.supplyAsync(() -> {
if (age < 0) {
throw new IllegalArgumentException("Age can not be negative");
}
if (age > 18) {
return "Adult";
} else {
return "Child";
}
}).exceptionally(ex -> {
System.out.println("Oops! We have an exception – " + ex.getMessage());
return "Unknown!";
});
exceptionFuture.thenAccept(System.out::println); //Unknown!
}

ii) Handle exceptions using the generic handle() method

The API also provides a more generic method – handle() to recover from exceptions. It is called whether or not an exception occurs. If an exception occurs, then the result argument will be null, otherwise, the ex argument will be null.



This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


private static void exceptionUsingHandle() {
Integer age = -1;
CompletableFuture<String> exceptionFuture = CompletableFuture.supplyAsync(() -> {
if (age < 0) {
throw new IllegalArgumentException("Age can not be negative");
}
if (age > 18) {
return "Adult";
} else {
return "Child";
}
}).handle((result, ex) -> {
if (ex != null) {
System.out.println("Oops! We have an exception – " + ex.getMessage());
return "Unknown!";
}
return result;
});
exceptionFuture.thenAccept(System.out::println); // Unknown!
}

Here is the link for the demo code and link for my previous blog.

References :

knoldus-advt-sticker

1 thought on “Future vs CompletableFuture in Java- #24 min read

  1. Java 8 has some exciting features at the JVM level and language level. Absolute must to know features of Java 8 are :
    1. Lambda Expressions
    2. Parallel Operations
    3. Introduction of Nashorn
    4. New Date/Time API’s
    5. Concurrent Accumulators

Comments are closed.

Discover more from Knoldus Blogs

Subscribe now to keep reading and get access to the full archive.

Continue reading