CompletableFuture in Java
Introduction
In modern software development, applications must be highly responsive and scalable. Blocking the main execution thread to wait for a database query, a network call, or a heavy computation is an anti-pattern that leads to poor user experiences and resource bottlenecks.
Introduced in Java 8, CompletableFuture revolutionized asynchronous programming in Java. It implements the Future and CompletionStage interfaces, providing a powerful, functional-style API to handle asynchronous computations, chain multiple tasks, and manage exceptions without the "callback hell" commonly found in older asynchronous models.
1. The Limitations of Traditional Future
Before Java 8, the java.util.concurrent.Future interface was used to represent the result of an asynchronous computation. However, it had severe limitations:
- Blocking Retrieval: To get the result, you had to call
future.get(), which blocked the current thread until the computation completed. - No Chaining: You could not execute a task automatically when the
Futurecompleted. - No Combining: There was no elegant way to combine multiple
Futureresults. - No Exception Handling: Handling errors inside a
Futurewas clumsy.
CompletableFuture solves all these problems by allowing you to attach callbacks and transformations that execute non-blocking.
2. Creating a CompletableFuture
You can create a CompletableFuture and supply it with a task to run asynchronously. By default, it uses the ForkJoinPool.commonPool(), but you can provide your own Executor.
runAsync (No Return Value)
Use runAsync when you want to execute a task in the background that doesn't return a result.
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("Running background task in: " + Thread.currentThread().getName());
// Simulate long-running task
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) {}
});
future.join(); // Blocks until complete (for demonstration purposes)
supplyAsync (Returns a Value)
Use supplyAsync when you need the background task to return a value.
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("Fetching data in: " + Thread.currentThread().getName());
return "Hello from CompletableFuture!";
});
// Non-blocking callback
future.thenAccept(result -> System.out.println("Result: " + result));
3. Chaining Tasks (The CompletionStage API)
The true power of CompletableFuture lies in its ability to chain tasks fluently.
thenApply (Transforming Data)
Takes the result of the previous stage, transforms it, and returns a new CompletableFuture.
CompletableFuture.supplyAsync(() -> "User123")
.thenApply(userId -> fetchUserProfile(userId))
.thenApply(profile -> profile.getEmail())
.thenAccept(email -> System.out.println("Sending email to: " + email));
thenAccept (Consuming Data)
Takes the result of the previous stage and consumes it without returning anything (Void).
thenRun (Running After Completion)
Executes a Runnable after the previous stage completes. It does not have access to the result of the previous stage.
CompletableFuture.supplyAsync(() -> performTask())
.thenRun(() -> System.out.println("Task completed successfully!"));
thenCompose (Chaining Dependent Futures)
If your transformation function returns a CompletableFuture itself, thenApply would result in a nested CompletableFuture<CompletableFuture<T>>. thenCompose flattens this, similar to flatMap in Streams.
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> getUser());
CompletableFuture<String> emailFuture = userFuture
.thenCompose(user -> fetchUserEmailAsync(user.getId()));
4. Combining Multiple Futures
Often, you need to run multiple asynchronous tasks in parallel and combine their results.
thenCombine (Combine Two Independent Futures)
Runs two futures independently and combines their results once both are complete.
CompletableFuture<Integer> weightFuture = CompletableFuture.supplyAsync(() -> 75);
CompletableFuture<Integer> heightFuture = CompletableFuture.supplyAsync(() -> 180);
CompletableFuture<Double> bmiFuture = weightFuture.thenCombine(heightFuture, (weight, height) -> {
double heightInMeters = height / 100.0;
return weight / (heightInMeters * heightInMeters);
});
allOf (Wait for Many Futures)
Waits for an array of CompletableFuture objects to all complete.
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Task 1");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Task 2");
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> "Task 3");
CompletableFuture<Void> allFutures = CompletableFuture.allOf(future1, future2, future3);
allFutures.thenRun(() -> System.out.println("All tasks finished!"));
anyOf (First to Complete)
Returns a new CompletableFuture that completes when any of the given futures complete, returning the result of the fastest one.
5. Exception Handling
Asynchronous code is notorious for hiding exceptions. CompletableFuture provides elegant ways to handle them.
exceptionally
Acts like a catch block. It allows you to recover from an exception by providing a default value.
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
if (true) throw new RuntimeException("API Failure");
return 200;
}).exceptionally(ex -> {
System.out.println("Error occurred: " + ex.getMessage());
return 0; // Default fallback value
});
handle
Runs regardless of success or failure (like a finally block), allowing you to handle the result or the exception in one place.
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return "Success Data";
}).handle((result, ex) -> {
if (ex != null) {
return "Recovered from: " + ex.getMessage();
}
return result;
});
Summary
CompletableFuture is an indispensable tool in modern Java development for building non-blocking, asynchronous systems. By mastering its functional-style API, you can write clean, concurrent code that efficiently utilizes system resources, elegantly chains complex workflows, and gracefully handles unexpected errors.