CompletableFuture is part of Java’s java.util.concurrent package introduced in Java 8. It is a powerful tool for asynchronous programming, allowing non-blocking execution, chaining of tasks, and efficient handling of results and exceptions.
Key Features of CompletableFuture
- Asynchronous Execution:
- Execute tasks in separate threads without blocking the main thread.
- Task Chaining:
- Chain multiple stages of computation using methods like
thenApply,thenAccept, orthenCompose.
- Chain multiple stages of computation using methods like
- Exception Handling:
- Handle exceptions gracefully using methods like
exceptionallyandhandle.
- Handle exceptions gracefully using methods like
- Combining Futures:
- Combine multiple
CompletableFutureinstances usingthenCombine,thenAcceptBoth,allOf, oranyOf.
- Combine multiple
- Non-blocking API:
- Unlike
Future,CompletableFutureprovides methods to retrieve results asynchronously.
- Unlike
Key Methods in CompletableFuture
| Method | Description |
|---|---|
supplyAsync(Supplier) | Executes a Supplier task asynchronously and returns a CompletableFuture. |
runAsync(Runnable) | Executes a Runnable task asynchronously without returning a result. |
thenApply(Function) | Transforms the result of a previous computation. |
thenAccept(Consumer) | Consumes the result without returning a value. |
thenCompose(Function) | Chains dependent asynchronous tasks. |
thenCombine(BiFunction) | Combines the result of two futures. |
thenAcceptBoth(Consumer) | Consumes results of two futures. |
exceptionally(Function) | Handles exceptions in a computation. |
allOf(CompletableFuture) | Combines multiple futures and completes when all are done. |
anyOf(CompletableFuture) | Completes when any one of the futures completes. |
handle(BiFunction) | Handles both result and exception. |
Scenarios Where CompletableFuture is Useful
- Asynchronous API Calls:
- For executing tasks like HTTP requests without blocking the main thread.
- Data Processing Pipelines:
- Chain multiple stages of data processing.
- Combining Multiple Results:
- Combine results from different asynchronous computations.
- Graceful Error Handling:
- Handle and recover from errors without interrupting the flow.
- Parallel Task Execution:
- Execute independent tasks concurrently and synchronize them efficiently.
Example 1: Basic CompletableFuture
Code:
import java.util.concurrent.CompletableFuture;
public class CompletableFutureBasicExample {
public static void main(String[] args) {
CompletableFuture.supplyAsync(() -> "Hello")
.thenApply(result -> result + " World!")
.thenAccept(System.out::println);
}
}
Explanation:
supplyAsync: Asynchronously provides the string “Hello”.thenApply: Transforms the result by appending ” World!”.thenAccept: Consumes the final result and prints it.
Output:
Hello World!
Example 2: Running a Task Without a Result
Code:
import java.util.concurrent.CompletableFuture;
public class CompletableFutureRunAsyncExample {
public static void main(String[] args) {
CompletableFuture.runAsync(() -> {
System.out.println("Task is running asynchronously.");
});
}
}
Explanation:
runAsync: Executes a task asynchronously without returning any result.
Output:
Task is running asynchronously.
Example 3: Handling Exceptions
Code:
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExceptionHandling {
public static void main(String[] args) {
CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) {
throw new RuntimeException("Something went wrong!");
}
return "Success";
}).exceptionally(ex -> {
System.out.println("Error: " + ex.getMessage());
return "Default Value";
}).thenAccept(System.out::println);
}
}
Explanation:
supplyAsync: Executes a task that may throw an exception.exceptionally: Catches the exception and provides a default value.thenAccept: Consumes the final result and prints it.
Output (Randomized):
Error: Something went wrong! Default Value
or
Success
Example 4: Combining Results of Two Futures
Code:
import java.util.concurrent.CompletableFuture;
public class CompletableFutureCombineExample {
public static void main(String[] args) {
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");
future1.thenCombine(future2, (result1, result2) -> result1 + " " + result2)
.thenAccept(System.out::println);
}
}
Explanation:
future1andfuture2: Asynchronously compute two strings.thenCombine: Combines the results of both futures.thenAccept: Prints the combined result.
Output:
Hello World
Example 5: Running Tasks in Parallel
Code:
import java.util.concurrent.CompletableFuture;
public class CompletableFutureParallelExample {
public static void main(String[] args) throws InterruptedException {
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
System.out.println("Task 1 running");
});
CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
System.out.println("Task 2 running");
});
CompletableFuture<Void> combined = CompletableFuture.allOf(future1, future2);
combined.join();
}
}
Explanation:
runAsync: Executes two tasks asynchronously.allOf: Waits for both tasks to complete.join: Blocks the main thread until all tasks are done.
Output:
Task 1 running Task 2 running
Example 6: Handling Both Results and Exceptions
Code:
import java.util.concurrent.CompletableFuture;
public class CompletableFutureHandleExample {
public static void main(String[] args) {
CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) {
throw new RuntimeException("Something failed");
}
return "Success";
}).handle((result, ex) -> {
if (ex != null) {
return "Handled error: " + ex.getMessage();
} else {
return "Result: " + result;
}
}).thenAccept(System.out::println);
}
}
Explanation:
handle: Processes both successful results and exceptions.thenAccept: Prints the final outcome.
Output (Randomized):
Handled error: Something failed
or
Result: Success
Advantages of CompletableFuture
- Non-blocking API:
- Enables better resource utilization compared to blocking threads.
- Efficient Chaining:
- Reduces boilerplate code for chaining dependent tasks.
- Integrated Exception Handling:
- Gracefully manage errors and fallback logic.
- Parallelism:
- Execute multiple independent tasks in parallel.
Disadvantages of CompletableFuture
- Complexity:
- Managing complex workflows with multiple tasks can lead to nested and hard-to-read code.
- Debugging:
- Debugging asynchronous code can be more challenging than synchronous code.
Summary
CompletableFuture is a robust and versatile tool for asynchronous programming in Java. Its ability to handle complex workflows with minimal blocking makes it an essential component of modern Java applications. By leveraging its features, developers can write efficient and scalable asynchronous code.