Technology
CompletableFuture is used for asynchronous programming in Java. Asynchronous Programming means running tasks in a separate thread, other than the main thread, and notifying the execution progress like completion or failure.
It helps improve application performance, as it executes separately from the main thread.
Comparison between Future and CompletableFuture:
CompletableFuture is an extension to Java Future API. Future is the return type of the method of an asynchronous execution. Future provides isDone()to check the completion status of the task, and the get() method is used to retrieve the output/ return value of the execution.
Future is the first implementation to support Asynchronous Programming in Java, and it comes with some limitations and some useful features.
Limitations of Future API:
1. Manual Completion method
Suppose the requested some Rest API, and it is taking too long time to receive the message. We can not set the cached response and mark the task is done.
2. Notification to the main execution thread
Future will not notify the main execution thread after it completes the execution. It provides a get() method, which will block the main thread until it completes and provides a response. The Future API does not provide any callback methods that will be called on the success/failure of a task completion condition.
3. Multiple Futures can not chain together:
4. Combining Multiple Future
Let’s say we are executing 10 tasks in parallel using Future API, and we want to execute some piece of code once all these tasks are completed, this feature is not provided in Future.
5. Exception Handling
Future API does not provide any exception handling.
All these limitations are implemented by extending the Future in CompletableFuture.This Class implements both the Future and CompletionStage interface.
CompletionStage interface
A stage of a possibly asynchronous computation, that performs an action or computes a value when another CompletionStage completes. A phase completes at the end of its computation, but this in turn can trigger other dependent phases. The functionality defined in this interface takes only a few basic forms, which expand to a larger set of methods to capture a range of usage styles.
All methods adhere to the triggering, execution, and exceptional completion specifications Additionally, while arguments used to pass a completion result (that is, for parameters of type T) for methods accepting them may be null, passing a null value for any other parameter will result in a NullPointerException being thrown.
1. Basic Example to create CompletableFuture:
CompletableFuture<String> future = new CompletableFuture<String>(); future.complete("result!!"); String result = future.get();
Future is the object to execute the empty task, and we mark the task as complete with a specified string, later we are invoking the get() method to get a return of the task. Any subsequent calls to the future object will be ignored because the function was completed.
2. Asynchronously running Tasks using runAsync():
CompletableFuture<Void>completableFuture = CompletableFuture.runAsync(new Runnable() { @Override public void run() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("executing using runAsync() method"); } }); completableFuture.get();
The same can be written as below using lambdas:
CompletableFuture<Void>completableFuture = CompletableFuture.runAsync(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("executing using runAsync() method"); }); completableFuture.get();
runAsync() method takes Runnable implementation and returns the void type of CompletableFuture.
3. If the CompletableFuture needs to inform the main thread with some return value then we can use the supplyAsync() method.
CompletableFuture<String>completableFuture = CompletableFuture.supplyAsync(() - > { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } return "executing using supplyAsync() method"; }); String returnValue = completableFuture.get(); System.out.println(returnValue);
Chaining CompletableFutures:
thenApply(): thenApply() is the method useful to execute some function after returning the completableFuture. It’s like a callback for CompletableFuture, this callback function will be executed by the main thread after CompletableFuture returns the value to the main thread.
CompletableFuture<String>firstName = CompletableFuture.supplyAsync(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } return "sravan"; }); CompletableFuture<String>fullName = firstName.thenApply(fName ->fName+" Kumar"); System.out.println(fullName.get());
thenApplyAsync(): This method will do the same as the thenApply() method, but the difference is callback will be executed in a separate thread, whereas thenApply() will be executed by the main thread.
CompletableFuture<String>firstName = CompletableFuture.supplyAsync(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } return "sravan"; }); CompletableFuture<String>fullName = firstName.thenApplyAsync(fName ->fName+" Kumar"); System.out.println(fullName.get());
We can chain thenApply() methods to any number, return the value of the previous thenApply() method value is input next thenApply() method and it goes.
thenAccept(): If the callback functions don’t want to return anything then thenAccept() and thenRun() methods will be used.
The difference between these is thenAccept() will take value from the previous execution, but thenRun() will not depend on the previous execution.
CompletableFuture<String>firstName = CompletableFuture.supplyAsync(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } return "sravan"; }); CompletableFuture<Void>fullName = firstName.thenAccept(fName -> { System.out.println(fName +" Kumar"); }); fullName.get();
Same method signature is available for Async style also.
Chaining CompletableFutures: Let’s say that we have 2 applications, one provides the user information, other provides the user’s credit card information. If we want to fetch the user’s credit card information parallelly, then we can use the thenApply() method, like:
CompletableFuture<User>getUsersDetail(String userId) { return CompletableFuture.supplyAsync(() -> { UserService.getUserDetails(userId); }); } CompletableFuture<Double>getCreditRating(User user) { return CompletableFuture.supplyAsync(() -> { CreditRatingService.getCreditRating(user); }); } CompletableFuture<CompletableFuture<Double>> result = getUserDetail(userId).thenApply(user ->getCreditRating(user));
But the return type CompletableFuture of CompletableFuture. In these cases, CompletabelFuture is providing the thenCompose() method for returning CompletableFuture of Object type.
CompletableFuture<Double> result = getUserDetail(userId).thenCompose(user ->getCreditRating(user));
thenCombine(): we can combine two independent CompletableFutures with the thenCombine() method.
CompletableFuture<String>firstName = CompletableFuture.supplyAsync(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } return "Sravan"; }); CompletableFuture<String>lastName = CompletableFuture.supplyAsync(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } return "Kumar"; }); CompletableFuture<String>fullName = firstName.thenCombine(lastName, (fName, lName) ->fName + " " + lName); System.out.println("fullName - " + fullName.get());
applyToEither(): Let us use this method if we want to execute some method for the first result between two CompletableFuture.
CompletableFuture<String>firstName = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Sravan";
});
CompletableFuture<String>lastName = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Kumar";
});
CompletableFuture<String>fullName = firstName.applyToEither(lastName, name -> name);
System.out.println("first returned - " + fullName.get());
acceptEither(): it is same as applyToEither() but it will not return any value.
CompletableFuture<String>firstName = CompletableFuture.supplyAsync(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } return "Sravan"; }); CompletableFuture<String>lastName = CompletableFuture.supplyAsync(() -> { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } return "Kumar"; }); CompletableFuture<Void>fullName = firstName.acceptEither(lastName, name -> { System.out.println(name); });
Exception Handling in CompletableFuture:
Similar thenApply() successful callback, we have one more callback for exception, exceptionally() method.
Integer age = -1; CompletableFuture<String>maturityFuture = 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!"; }); System.out.println("Maturity: " + maturityFuture.get());
We can also combine successful and error callbacks using the handle method, which takes two parameters, success value and exception. If the exception contains value means a task-thrown exception.
Conclusion
We explored the most commonly used and important concepts of CompletableFuture introduced in Java, and we also learned the differences between Future and CompletableFuture.