Java: Asynchronous programming 101
In modern software development, the ability to handle multiple tasks simultaneously is becoming increasingly important.
Asynchronous programming, which allows a program to continue executing while waiting for a long-running task to
complete, is an essential technique for achieving this. In this blog post, we will explore asynchronous programming in
Java using the CompletableFuture
class.
What is CompletableFuture?
CompletableFuture
is a class introduced in Java 8 that represents a future result of an asynchronous computation. It
can be used to execute tasks asynchronously and return a result when the task completes. CompletableFuture
provides
several methods to compose, combine and transform its results, which makes it a powerful tool for writing asynchronous
code.
To create a CompletableFuture
, we can use the static factory methods in the CompletableFuture
class. For example, to
create a CompletableFuture
that returns a string, we can use the following code:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// some long-running computation
return "result";
});
This code creates a CompletableFuture
that executes a long-running computation asynchronously and returns a string
result when the computation completes.
When a CompletableFuture
completes, we can retrieve its result using the get method. However, calling the get
method
blocks the current thread until the result is available. A better way to handle the result is to use the thenAccept
method, which allows us to specify a callback function that is invoked when the CompletableFuture
completes.
For example, the following code shows how to use the thenAccept
method to print the result of the CompletableFuture
:
public CompletableFuture<Integer> externalApiCall() {
return CompletableFuture.supplyAsync(() -> {
int val = new Random().nextInt(100);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return val;
});
}
externalApiCall()
.thenAccept(val -> System.out.println("Returned Value: " + val));
We can chain multiple CompletableFutures together to execute a series of asynchronous tasks. When one CompletableFuture
completes, we can use its result to start the next CompletableFuture
. We can chain CompletableFutures using the
thenCompose
or thenCombine
method. Example,
public CompletableFuture<Integer> externalApiCall() {
return CompletableFuture.supplyAsync(() -> {
int val = new Random().nextInt(100);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return val;
});
}
externalApiCall()
// Converting Integer to String using thenCompose
.thenCompose(val -> CompletableFuture.completedFuture(String.valueOf(val)))
.thenAccept(val -> System.out.println("Returned Value: " + val));
externalApiCall()
// Returns sum of first and 2nd CompletableFuture by thenCombine
.thenCombine(externalApiCall(), Integer::sum)
.thenAccept(val -> System.out.println("Returned Value: " + val));
Besides, CompletableFuture provides several methods for handling exceptions that may occur during the execution of an asynchronous task. We can use the exceptionally method to specify a callback function that is invoked when an exception occurs. We can also use the handle method to specify a callback function that is invoked regardless of whether the CompletableFuture completes normally or exceptionally.
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
// some long-running computation that may throw an exception
throw new RuntimeException("exception");
});
future.exceptionally(ex -> {
System.out.println("Exception: " + ex.getMessage());
return 0;
});
future.thenAccept(result -> System.out.println("Result: " + result);
That’s it.