Last Updated on July 31, 2024
Java’s Future interface is part of the java.util.concurrent package and represents the result of an asynchronous computation. It was introduced in Java 5 (2004) as part of the concurrency utilities.
Future represents the result of an asynchronous computation that may not be available yet. It provides methods to check if the computation is complete, wait for its completion, and retrieve the result. However, it offers limited functionality.
CompletableFuture builds upon Future, adding the ability to chain asynchronous computations, handle exceptions, and perform various asynchronous operations. It’s a more powerful and flexible tool for managing asynchronous tasks. It allows you to create complex asynchronous workflows with features like thenApply, thenCompose, and exceptionally.
In essence, Future is a promise of a result, while CompletableFuture is a promise with additional capabilities for handling and combining asynchronous computations.
Future Interface
A Future in Java represents the result of an asynchronous computation. It’s a placeholder for a value that will be available at some point in the future. The Future interface provides methods to check if the computation is complete, wait for its completion, and retrieve the result.
Using Future allows you to submit tasks for execution and continue with other work, retrieving the result when needed. This is essential for asynchronous programming and improving application responsiveness.
A task (usually a Callable or Runnable) is submitted to an ExecutorService.
Task Creation:
Callable<Integer> task = () -> {
// Simulate work
Thread.sleep(2000);
return 42;
};
Submission:
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(task);
The task runs in the background while the main thread continues.
Retrieve result:
try {
Integer result = future.get();
System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
Putting it all together:
import java.util.concurrent.*;
public class FutureExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(() -> {
// Simulate a long-running task
Thread.sleep(2000);
return 42;
});
try {
System.out.println("Task started...");
while (!future.isDone()) {
System.out.println("Task is still running...");
Thread.sleep(500);
}
// Check if the task is done
System.out.println("Is task done? " + future.isDone());
// Wait for the result and retrieve it
Integer result = future.get();
System.out.println("Task result: " + result);
// Check again if the task is done
System.out.println("Is task done? " + future.isDone());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
}
This example demonstrates creating a Future, submitting a task to an ExecutorService, checking its status, and retrieving the result. The task simulates a long-running operation by sleeping for 2 seconds before returning a value.
It is important to remark that get() method blocks the execution until the future is complete.
The Future
interface in Java, while essential for asynchronous programming, presents certain constraints.
Its blocking get()
method can hinder performance and responsiveness, especially when dealing with multiple futures. Additionally, the Future
interface lacks built-in mechanisms for chaining asynchronous operations or handling exceptions in a streamlined manner.
These limitations have led to the development of more advanced asynchronous constructs like CompletableFuture
.
CompletableFuture
CompletableFuture is an enhanced implementation of the Future interface introduced in Java 8. It implements both Future and CompletionStage interfaces, offering a rich set of methods for various scenarios.
It provides a more powerful and flexible way to work with asynchronous computations. Unlike regular Futures, CompletableFuture allows for chaining multiple asynchronous operations, combining results from multiple futures, and handling exceptions more gracefully.
This chaining is achieved through methods like thenApply(), thenCompose(), and thenCombine(), which enable the creation of complex asynchronous workflows.
CompletableFuture supports non-blocking operations, improving application responsiveness. It provides robust exception handling mechanisms, allowing developers to handle errors gracefully using methods like exceptionally() and handle(). The API includes support for completion stages, where actions can be defined to execute upon task completion.
It also combines results from multiple futures, facilitating the aggregation of data from various asynchronous sources. CompletableFuture offers flexible execution options, allowing tasks to run using custom executors or the common fork-join pool.
The API includes methods for timeout handling and cancellation, enhancing control over long-running tasks. It also provides ways to create already completed futures, useful for testing and specific use cases.
CompletableFuture Example
Here’s a sample implementation demonstrating some key features of CompletableFuture:
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class CompletableFutureExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// Create and chain multiple CompletableFutures
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// Simulate some work
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "Hello";
}).thenApply(result -> result + " CompletableFuture")
.thenApply(String::toUpperCase);
// Add an exception handler
future = future.exceptionally(ex -> "An error occurred: " + ex.getMessage());
// Add a completion action
future.thenAccept(result -> System.out.println("Result: " + result));
// Wait for the future to complete
future.join();
// Combine two futures
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 20);
CompletableFuture<Integer> combinedFuture = future1.thenCombine(future2, Integer::sum);
System.out.println("Combined result: " + combinedFuture.get());
}
}
In the above code:
supplyAsync()
method Creates a CompletableFuture that runs a task asynchronously.
thenApply()
method Chains operations. Each thenApply()
transforms the result of the previous stage.
exceptionally()
method handles exceptions that might occur in the CompletableFuture chain.
thenAccept()
method defines an action to be performed when the CompletableFuture completes.
join()
method waits for the CompletableFuture to complete. Similar to get()
but doesn’t throw checked exceptions.
Second pat=rt of the code combines two futures
thenCombine()
method combines the results of two CompletableFutures once both are complete.
The output of the code will be:
Result: HELLO COMPLETABLEFUTURE
Combined result: 30
Conclusion
While Futures remain useful for simpler asynchronous tasks, CompletableFutures are generally preferred for more complex scenarios. They align well with modern programming paradigms and can significantly improve the structure and efficiency of concurrent code.
In practice, CompletableFuture is particularly valuable in scenarios involving microservices, where multiple independent operations need to be coordinated. It is also beneficial in UI applications for managing background tasks without freezing the interface, and in data processing pipelines where operations can be parallelized.
As applications increasingly demand responsiveness and the ability to handle multiple operations concurrently, mastering these tools becomes crucial for Java developers.
The shift towards CompletableFutures represents a move towards more declarative and functional approaches to concurrency, aligning with broader trends in software development.