Published: November 20, 2016
Introduction
Asynchronous programming has become an essential feature in modern applications, especially when dealing with IO-bound tasks or tasks that involve waiting for external systems (like databases or APIs). In Java 8, the CompletableFuture class was introduced to simplify asynchronous programming.
In this tutorial, we will explore:
- What CompletableFuture is and how it works.
- How to create and manage asynchronous tasks.
- How to combine multiple asynchronous tasks and handle their results.
By the end of this tutorial, you’ll understand how to use CompletableFuture for writing more efficient and scalable Java applications.
What is CompletableFuture?
CompletableFuture
is part of the java.util.concurrent package and represents a future result of an asynchronous computation. It is a non-blocking alternative to Future
and ExecutorService
. The CompletableFuture
class allows you to:
- Run tasks asynchronously.
- Chain multiple tasks together.
- Combine results from multiple tasks.
It provides powerful methods for both composing and combining asynchronous computations in a more readable and structured way.
Basic Usage of CompletableFuture
Let’s start with a basic example that shows how to create and run an asynchronous task using CompletableFuture
:
import java.util.concurrent.*;
public class CompletableFutureExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// Create an asynchronous task
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000); // Simulate long-running task
} catch (InterruptedException e) {
e.printStackTrace();
}
return 42; // Return a result
});
// Wait for the result (blocking)
Integer result = future.get(); // This blocks until the task completes
System.out.println("Result: " + result);
}
}
Explanation:
- We create a
CompletableFuture
that runs an asynchronous task using thesupplyAsync()
method. - The
get()
method is called to block and wait for the result of the asynchronous computation.
Output:
Result: 42
Chaining Tasks with CompletableFuture
One of the powerful features of CompletableFuture
is the ability to chain multiple tasks together using methods like thenApply()
, thenAccept()
, or thenCompose()
. Chaining tasks allows you to process the result of one task and pass it to another task without blocking the main thread.
Example: Chaining Multiple Tasks
import java.util.concurrent.*;
public class CompletableFutureExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// Start an asynchronous task
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000); // Simulate task
} catch (InterruptedException e) {
e.printStackTrace();
}
return 10;
});
// Chain another task to process the result
CompletableFuture<Integer> resultFuture = future.thenApplyAsync(result -> {
System.out.println("Processing the result: " + result);
return result * 2;
});
// Chain another task
resultFuture.thenAcceptAsync(result -> {
System.out.println("Final result after processing: " + result);
});
// Wait for all tasks to complete
resultFuture.get();
}
}
Explanation:
- We first use
supplyAsync()
to start an asynchronous task. - The
thenApplyAsync()
method is used to process the result of the previous task and return a new result. - The
thenAcceptAsync()
method is used to perform an action with the final result (in this case, printing it).
Output:
Processing the result: 10
Final result after processing: 20
Handling Errors in CompletableFuture
In asynchronous programming, it’s essential to handle errors properly. CompletableFuture
provides several methods, such as exceptionally()
and handle()
, to handle exceptions and recover from errors gracefully.
Example: Handling Exceptions
import java.util.concurrent.*;
public class CompletableFutureExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// Start an asynchronous task that throws an exception
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("Something went wrong!");
});
// Handle the exception
CompletableFuture<Integer> resultFuture = future.exceptionally(ex -> {
System.out.println("Error occurred: " + ex.getMessage());
return -1; // Return a default value
});
// Wait for the result
Integer result = resultFuture.get();
System.out.println("Result after handling exception: " + result);
}
}
Explanation:
- In this example, the task throws an exception, but we use
exceptionally()
to catch the exception and return a default value (-1).
Output:
Error occurred: Something went wrong!
Result after handling exception: -1
Combining Multiple CompletableFuture Tasks
Sometimes you need to combine results from multiple asynchronous tasks. CompletableFuture
provides methods like thenCombine()
, allOf()
, and anyOf()
to help you with such cases.
Example: Combining Results Using thenCombine()
import java.util.concurrent.*;
public class CompletableFutureExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// First asynchronous task
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
return 10;
});
// Second asynchronous task
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
return 20;
});
// Combine results from both tasks
CompletableFuture<Integer> combinedFuture = future1.thenCombine(future2, (result1, result2) -> {
return result1 + result2;
});
// Wait for the result
Integer result = combinedFuture.get();
System.out.println("Combined result: " + result);
}
}
Explanation:
- We create two asynchronous tasks (
future1
andfuture2
). - The
thenCombine()
method is used to combine the results of both tasks. The lambda expression adds the results together.
Output:
Combined result: 30
Conclusion
CompletableFuture
is a powerful tool for asynchronous programming in Java. It allows you to run tasks asynchronously, handle errors, and combine multiple tasks in a clean and readable manner.
In this tutorial, you learned how to:
- Run asynchronous tasks using
CompletableFuture
. - Chain tasks together using
thenApplyAsync()
,thenAcceptAsync()
. - Handle exceptions with
exceptionally()
andhandle()
. - Combine results from multiple tasks using
thenCombine()
.
In the next tutorial, we will dive into the usage of flatMap() in Java 8’s Streams API to handle more complex data structures.