CompletableFuture in Java 8
Introduction
Java 8 introduced CompletableFuture
, a powerful enhancement to the java.util.concurrent
package,
designed to simplify asynchronous programming and enable complex concurrent operations in a more readable
and maintainable manner. This article will delve into the core features of CompletableFuture,
its practical applications, and how it can be leveraged to improve concurrency in Java applications.
What is CompletableFuture?
CompletableFuture is a class that represents a future result of an asynchronous computation.
It allows you to write non-blocking code by enabling you to attach callbacks
that will be executed upon the completion of a computation.
Unlike the traditional Future
interface, CompletableFuture
provides a more extensive API
for handling asynchronous operations, including the ability to combine multiple futures, handle exceptions,
and create complex asynchronous pipelines.
Key Features of CompletableFuture
-
Asynchronous Computation
You can start a computation asynchronously and define actions to be taken once the computation is complete. -
Chaining Operations
CompletableFuture supports chaining multiple operations, enabling you to build complex asynchronous workflows in a declarative manner. -
Exception Handling
It provides mechanisms to handle exceptions that occur during asynchronous computations. -
Combining Futures
You can combine multiple CompletableFuture instances, waiting for all of them to complete or completing when any of them finishes.
Creating a CompletableFuture
You can create a CompletableFuture using various factory methods:
Creating a Completed CompletableFuture
1
2
CompletableFuture<String> completedFuture = CompletableFuture.completedFuture("Hello, World!");
completedFuture.thenAccept(System.out::println); // Output: Hello, World!
Creating a CompletableFuture with a Supplier
1
2
3
4
CompletableFuture.supplyAsync(() -> {
// Simulate a long-running task
return "Hello from CompletableFuture!";
}).thenAccept(System.out::println); // Output: Hello from CompletableFuture!
Chaining Operations
CompletableFuture allows you to chain multiple operations using methods like thenApply
, thenAccept
,
and thenCompose
.
Using thenApply
thenApply
transforms the result of the CompletableFuture when it completes.
1
2
3
CompletableFuture.supplyAsync(() -> "Hello")
.thenApply(result -> result + ", World!")
.thenAccept(System.out::println); // Output: Hello, World!
Using thenAccept
thenAccept
performs an action with the result when the CompletableFuture completes.
1
2
CompletableFuture.supplyAsync(() -> 42)
.thenAccept(result -> System.out.println("Result is: " + result)); // Output: Result is: 42
Using thenCompose
thenCompose
is used to chain multiple CompletableFuture instances.
It is used when the next stage depends on the result of the previous stage and returns a new CompletableFuture.
1
2
3
CompletableFuture.supplyAsync(() -> "Hello")
.thenCompose(result -> CompletableFuture.supplyAsync(() -> result + ", World!"))
.thenAccept(System.out::println); // Output: Hello, World!
Exception Handling
CompletableFuture provides several methods for handling exceptions, including exceptionally
and handle
.
Using exceptionally
exceptionally
allows you to recover from exceptions that occur during asynchronous computation.
1
2
3
4
5
6
7
8
CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("Something went wrong!");
})
.exceptionally(ex -> {
System.out.println("Exception: " + ex.getMessage());
return "Fallback result";
})
.thenAccept(System.out::println); // Output: Exception: Something went wrong! Fallback result
Using handle
handle
allows you to handle both the result and the exception of a CompletableFuture.
1
2
3
4
5
6
7
8
9
10
11
CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("Error occurred!");
})
.handle((result, ex) -> {
if (ex != null) {
System.out.println("Exception: " + ex.getMessage());
return "Fallback result";
}
return result;
})
.thenAccept(System.out::println); // Output: Exception: Error occurred! Fallback result
Combining Multiple Futures
CompletableFuture provides methods to combine multiple futures, such as allOf
, anyOf
, and thenCombine
.
Using allOf
allOf
allows you to wait for multiple futures to complete.
1
2
3
4
5
6
7
8
9
10
11
12
CompletableFuture<Void> future1 = CompletableFuture.supplyAsync(() -> {
// Simulate a long-running task
return "Task 1";
}).thenAccept(System.out::println);
CompletableFuture<Void> future2 = CompletableFuture.supplyAsync(() -> {
// Simulate a long-running task
return "Task 2";
}).thenAccept(System.out::println);
CompletableFuture<Void> allOf = CompletableFuture.allOf(future1, future2);
allOf.join(); // Waits for all futures to complete
Using anyOf
anyOf
completes when any one of the provided futures completes.
1
2
3
4
5
6
7
8
9
10
11
12
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
sleep(1000);
return "Task 1";
});
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
sleep(500);
return "Task 2";
});
CompletableFuture<Object> anyOf = CompletableFuture.anyOf(future1, future2);
anyOf.thenAccept(result -> System.out.println("First completed: " + result)); // Output: First completed: Task 2
Helper method for sleeping:
1
2
3
4
5
6
7
private static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
Using thenCombine
thenCombine
combines the results of two futures when both are complete.
1
2
3
4
5
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 5);
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 3);
future1.thenCombine(future2, (result1, result2) -> result1 + result2)
.thenAccept(sum -> System.out.println("Sum: " + sum)); // Output: Sum: 8
Best Practices
-
Avoid Blocking Calls
Prefer non-blocking operations and avoid methods likeget()
which block until the future completes. -
Handle Exceptions Gracefully
Useexceptionally
andhandle
to manage exceptions and ensure that your application can recover from errors. -
Use CompletableFuture for Complex Pipelines
When dealing with complex asynchronous workflows, use chaining and combining methods to create clear and manageable pipelines. -
Consider Using CompletableFuture for Parallel Processing
Utilize CompletableFuture for parallel processing of tasks, especially when tasks are independent and can run concurrently.
Conclusion
CompletableFuture is a powerful tool in Java 8 that simplifies asynchronous programming and improves concurrency handling. By using CompletableFuture, developers can write more readable and maintainable code, efficiently manage asynchronous tasks, and build complex pipelines without resorting to nested callbacks. Mastering CompletableFuture can significantly enhance your ability to handle concurrency and improve the responsiveness of your Java applications.