3 minutes
Mastering Asynchronous Programming with Java’s CompletableFuture
Introduced in Java 8, CompletableFuture is a powerful class for asynchronous programming. It extends the traditional Future interface with a rich set of features for composing, combining, and handling asynchronous tasks, enabling you to write non-blocking, highly concurrent applications with ease.
Why CompletableFuture?
Before CompletableFuture, Java’s Future was limited. You could start an asynchronous task, but you had to block your main thread using get() to retrieve the result. This defeated the purpose of non-blocking code.
CompletableFuture solves this by providing a non-blocking, callback-based approach. It allows you to chain tasks together, so that a subsequent task automatically executes when the previous one completes, without ever blocking your main thread.
Core Features of CompletableFuture
- Asynchronous Execution: Run tasks in the background without blocking the caller thread, typically using a
ForkJoinPoolor a customExecutor. - Composability and Chaining: Chain multiple asynchronous operations together using methods like
thenApply(),thenAccept(), andthenCompose(). - Powerful Combination: Combine the results of multiple
CompletableFuturesusingthenCombine(),allOf(), oranyOf(). - Robust Exception Handling: Handle exceptions gracefully within the asynchronous pipeline using methods like
exceptionally()andhandle(). - Explicit Completion: Programmatically complete a future with a result or an exception, giving you more control over its lifecycle.
Key Methods of CompletableFuture
Here’s a look at some of the most commonly used methods:
| Method | Description |
|---|---|
supplyAsync(Supplier) |
Runs a task asynchronously that returns a result. |
runAsync(Runnable) |
Runs a task asynchronously that doesn’t return a result. |
thenApply(Function) |
Transforms the result of a CompletableFuture when it completes. |
thenAccept(Consumer) |
Consumes the result of a CompletableFuture when it completes. |
thenRun(Runnable) |
Executes a Runnable after a CompletableFuture completes. |
thenCombine() |
Combines the results of two CompletableFutures when both are done. |
exceptionally(Function) |
Provides a fallback value in case of an exception. |
join() |
Waits for the computation to complete and returns the result (throws an unchecked exception). |
get() |
Similar to join(), but throws checked exceptions. |
How Does CompletableFuture Work?
- Asynchronous Task Execution: When you call
supplyAsync()orrunAsync(), the task is submitted to a thread pool (by default, theForkJoinPool.commonPool()). Your main thread is free to continue its work. - Callback Chaining: You can attach callbacks using methods like
thenApply()orthenAccept(). These callbacks are registered and will be executed only when the preceding task finishes. - Result and Exception Handling: The
CompletableFutureobject holds the state of the computation (e.g., pending, completed with a result, or completed with an exception). When a task finishes, it triggers the execution of any dependent callbacks. - Combining Futures: Methods like
allOf()andanyOf()allow you to coordinate multiple asynchronous tasks, enabling you to proceed when all or any of them have completed.
Practical Example
Let’s see CompletableFuture in action with a simple example:
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// Simulate a long-running task
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Hello";
}).thenApply(result -> result + " World!");
// Attach a callback to consume the final result
future.thenAccept(result -> {
System.out.println("Result: " + result);
System.out.println("This is executed in thread: " + Thread.currentThread().getName());
});
System.out.println("This is printed immediately from the main thread.");
// Wait for the future to complete to see the output
future.join();
}
}
In this example:
supplyAsync()starts a background task.thenApply()chains another task to transform the result.thenAccept()registers a final callback to print the result.- The main thread prints its message immediately and then calls
join()to wait for the asynchronous pipeline to finish before exiting.
By leveraging CompletableFuture, you can build sophisticated, non-blocking, and highly efficient applications in Java.