Exploring the Benefits of Asynchronous Programming in Java 8

Asynchronous programming refers to the occurrence of events that are independent of the main program flow and ways to deal with such events. In this article we are going to look into new ways of processing data asynchronously using CompletableFuture class. CompletableFuture implements Future and CompletionStage classes.

public class CompletableFuture<T> extends Object implements Future<T>, CompletionStage<T>

A Future represents the result of an asynchronous computation. The biggest caveat of using Future in Java is that the result can only be retrieved using method get which is a blocking call, when the computation has completed.

public interface Future<V>

CompletionStage is a stage of a possibly asynchronous computation, that performs an action or computes a value when another CompletionStage completes.

public interface CompletionStage<T>

We're going to use lambda expressions extensively and if you have a good grasp on those then just follow along. Otherwise, I'd suggest you to read my tutorial on lambda expressions first and then come back to this article.

We will look into some of the important methods of the CompletableFuture class to help us understand how can we design asynchronous programming in java 8. If you would like to check other methods that are not discussed in this article you can check here CompletableFuture API. Let's start with the supplyAsync method which returns CompletableFuture <U>

final CompletableFuture<String> callString = CompletableFuture.supplyAsync(() -> "Some random String");

//following is a blocking call and an anti-pattern. try to avoid in your application.
final String stringValue = callString.get();

Instead of doing a get() call, we can use thenAccept() which returns a CompletableFuture\.

CompletableFuture<Void> acceptString = callString.thenAccept(str -> System.out.println(str));

But this is a callback hell, what if we wanted to do some transformation? Below code will fail:

CompletableFuture<Integer> acceptInteger = callString.thenAccept(str -> str.length());

Instead of thenAccept we can use thenApply which returns CompletableFuture<U&gt.

final CompletableFuture<Float> floatVal = callString.thenApply(str -> str.length()).thenApply(Integer -> (float)Integer);

Let's move onto a flatMap function, thenCompose. It requires a CompletableFuture and instead of nesting outputs like CompletableFuture\<CompletableFuture\> it returns CompletableFuture\. OK, let's assume we have a function getPrices function which returns a CompletableFuture\.

CompletableFuture<Double> prices = callString.thenCompose(str -> getPrices(Double.parseDouble(str)));

Let's do zip functions now. We will combine two futures using thenCombine. Basically parallel tasks that are independent, so we can run these parallely and increase the throughput and reduce latency.

//assume getDiscount returns CompletableFuture<Double>
CompletableFuture<Double> discount = getDiscount(5.0);
CompletableFuture<Double> finalPrice = prices.thenCombine(discount, (price, disc) -> price - disc);

The opposite of thenCombine is applyToEither method. It will be applied to the first one that is returned. One of the use cases could be that we have two servers which return the same result and we want get the one that is returned first.

CompletableFuture<Double> firstPrice = prices.toApplyEither(discount, price -> price);

Moving onto "when things go wrong", how do we handle it? Well, there are two ways to handle exceptions using CompletableFutures API: handle and exceptionally. Let's look at both.

final CompletableFuture<String> recoverByHandle = callString.handle((result, throwable) -> 
    {if(throwable != null) return "Something wrong happened, returning throwable: "+ throwable;
    else return result.toLowerCase();
    }); 

final CompletableFuture<String> recoverExceptionally = 
    callString.exceptionally(throwable -> "Something wrong happened, try again later");

So what's the difference between handle and exceptionally?

Method handle additionally allows the stage to compute a replacement result that may enable further processing by other dependent stages. In all other cases, if a stage's computation terminates abruptly with an (unchecked) exception or error, then all dependent stages requiring its completion complete exceptionally as well, with a CompletionException holding the exception as its cause. If a stage is dependent on both of two stages, and both complete exceptionally, then the CompletionException may correspond to either one of these exceptions. If a stage is dependent on either of two others, and only one of them completes exceptionally, no guarantees are made about whether the dependent stage completes normally or exceptionally. In the case of method whenComplete, when the supplied action itself encounters an exception, then the stage exceptionally completes with this exception if not already completed exceptionally.

We just scratched the surface but this article will help you get up and running quickly with CompletableFuture API in java 8.

References: Future API Java, CompletionStage API Java, CompletableFuture API Java and CompletableFuture exceptionally method