Transform Future into CompletableFuture
In modern Java programming, handling asynchronous tasks efficiently is a critical skill. Java provides two key abstractions for dealing with asynchronous operations: Future and CompletableFuture. While Future has been part of the Java standard library since Java 5, it is relatively limited in functionality. Java 8 introduced CompletableFuture, a more versatile framework for asynchronous programming. This article explores how to transform a Future into a CompletableFuture, enabling the use of its advanced features like chaining, exception handling, and combining multiple asynchronous operations.
1. Using Future
The Future interface provides a straightforward way to handle asynchronous computation results. Here’s an example of how you can use Future to run a task asynchronously:
import java.util.concurrent.*;
import java.util.logging.Level;
import java.util.logging.Logger;
public class FutureExample {
private static final Logger log = Logger.getLogger(FutureExample.class.getName());
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
// Submit a task that simulates a long-running operation
Future<String> future = executor.submit(() -> {
Thread.sleep(2000); // Simulate a delay
return "Future Result";
});
try {
// Block and wait for the result
String result = future.get();
log.log(Level.INFO, "Task completed: {0}", result);
} catch (InterruptedException | ExecutionException e) {
log.info(e.getMessage());
} finally {
executor.shutdown();
}
}
}
Output
INFO: Task completed: Future Result
1.1 Limitations of Future
While the above example works, Future has several drawbacks:
- Blocking Nature: You must call
future.get()to retrieve the result, which blocks the thread until the computation is complete. - No Callbacks: There is no way to specify an action to perform once the result is available.
- No Chaining:
Futuredoesn’t support chaining or combining multiple asynchronous tasks. - Limited Error Handling: Exceptions need to be manually handled during
get().
Due to these limitations, we often transform Future to CompletableFuture to take advantage of its modern features.
2. Why Convert Future to CompletableFuture?
Transforming a Future to a CompletableFuture allows us to:
- Compose Tasks: Combine multiple asynchronous tasks easily with
thenCombine(),thenCompose(), etc. - Avoid Blocking: Use non-blocking APIs to handle results as soon as they are available.
- Enhance Readability: Write cleaner, chainable code using lambda expressions.
- Handle Errors Gracefully: Utilize
exceptionally()and other error-handling mechanisms.
3. Converting Future to CompletableFuture
Converting a Future to a CompletableFuture unlocks the flexibility and power of modern asynchronous programming in Java. It allows us to transition from blocking, cumbersome workflows to clean, non-blocking, and chainable operations. Whether through a background thread or a polling mechanism, this transformation bridges legacy code using Future with the advanced capabilities of CompletableFuture.
3.1 Approach 1: Using a Background Thread
The simplest way to convert a Future to a CompletableFuture is by using a background thread to monitor the Future. Here is a code example demonstrating this approach.
import java.util.concurrent.*;
public class FutureToCompletableFuture {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
Thread.sleep(2000);
return "Future Result";
});
// Convert Future to CompletableFuture
CompletableFuture<String> completableFuture = futureToCompletableFuture(future);
// Use CompletableFuture
completableFuture
.thenApply(result -> "Processed: " + result)
.thenAccept(System.out::println)
.exceptionally(ex -> {
System.out.println("Error: " + ex.getMessage());
return null;
});
executor.shutdown();
}
private static <T> CompletableFuture<T> futureToCompletableFuture(Future<T> future) {
CompletableFuture<T> completableFuture = new CompletableFuture<>();
Executors.newSingleThreadExecutor().submit(() -> {
try {
completableFuture.complete(future.get());
} catch (Exception e) {
completableFuture.completeExceptionally(e);
}
});
return completableFuture;
}
}
The above background thread conversion approach uses a separate thread to monitor the Future and complete a corresponding CompletableFuture when the Future computation finishes. The thread waits for the Future result using the get() method and completes the CompletableFuture with the retrieved value. This allows the blocking behaviour of Future to be encapsulated within the monitoring thread, enabling non-blocking workflows for the CompletableFuture.
If an exception occurs during the execution of the Future or while fetching its result, the CompletableFuture is completed exceptionally. This ensures errors are properly propagated and can be handled using the robust mechanisms provided by CompletableFuture.
Output
Processed: Future Result
3.2 Approach 2: Using Polling
Polling is another approach to converting a Future into a CompletableFuture. Instead of blocking, we periodically check whether the Future has completed.
import java.util.concurrent.*;
public class PollingConversion {
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
Thread.sleep(2000);
return "Future Result";
});
// Convert Future to CompletableFuture using Polling
CompletableFuture<String> completableFuture = pollFutureToCompletableFuture(future);
// Use CompletableFuture
completableFuture
.thenApply(result -> "Processed via Polling: " + result)
.thenAccept(System.out::println)
.exceptionally(ex -> {
System.out.println("Error: " + ex.getMessage());
return null;
});
executor.shutdown();
}
private static <T> CompletableFuture<T> pollFutureToCompletableFuture(Future<T> future) {
CompletableFuture<T> completableFuture = new CompletableFuture<>();
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
if (future.isDone()) {
try {
completableFuture.complete(future.get());
} catch (InterruptedException | ExecutionException e) {
completableFuture.completeExceptionally(e);
}
}
}, 0, 100, TimeUnit.MILLISECONDS); // Poll every 100ms
return completableFuture;
}
}
The polling approach utilizes a ScheduledExecutorService to periodically check if the Future has completed. This non-blocking method ensures that the CompletableFuture is only completed once the Future finishes, without holding up any threads. Additionally, the polling frequency is customizable, allowing us to set intervals, such as every 100 milliseconds, based on the specific requirements of the application.
Output
Processed via Polling: Future Result
4. Merging Multiple Future Tasks into One CompletableFuture
Sometimes, we may need to wait for the results of multiple Future objects and process them together. By combining these Future objects into a single CompletableFuture, we can aggregate their results and handle them more efficiently.
4.1 Why Combine Multiple Futures?
Combining multiple Future objects into a single CompletableFuture simplifies the management of asynchronous computations by reducing the complexity of handling each task individually. It allows developers to aggregate the results of several tasks into a single cohesive outcome, streamlining data processing. Additionally, this approach centralizes error handling, making it easier to manage exceptions across all tasks in a unified manner.
Below is an example code for combining multiple futures:
public class CombineFuturesExample {
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(3);
// Create multiple Futures
Future<String> future1 = executor.submit(() -> {
Thread.sleep(1000);
return "Result 1";
});
Future<String> future2 = executor.submit(() -> {
Thread.sleep(2000);
return "Result 2";
});
Future<String> future3 = executor.submit(() -> {
Thread.sleep(3000);
return "Result 3";
});
// Combine Futures into a CompletableFuture
CompletableFuture<List<String>> combinedFuture = combineFutures(List.of(future1, future2, future3));
// Process the combined result
combinedFuture.thenAccept(results
-> results.forEach(System.out::println)
).exceptionally(ex -> {
System.out.println("Error occurred: " + ex.getMessage());
return null;
});
executor.shutdown();
}
private static <T> CompletableFuture<List<T>> combineFutures(List<Future<T>> futures) {
List<CompletableFuture<T>> completableFutures = futures.stream()
.map(future -> futureToCompletableFuture(future))
.collect(Collectors.toList());
return CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0]))
.thenApply(v -> completableFutures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList())
);
}
private static <T> CompletableFuture<T> futureToCompletableFuture(Future<T> future) {
CompletableFuture<T> completableFuture = new CompletableFuture<>();
Executors.newSingleThreadExecutor().submit(() -> {
try {
completableFuture.complete(future.get());
} catch (InterruptedException | ExecutionException e) {
completableFuture.completeExceptionally(e);
}
});
return completableFuture;
}
}
In this approach, multiple Future objects are first created to represent individual asynchronous computations. Each Future is then transformed into a CompletableFuture, enabling the use of modern asynchronous features. These CompletableFuture instances are combined using CompletableFuture.allOf(), which aggregates their results into a single operation. Once all tasks have completed, the results are collected into a list for further processing, ensuring seamless handling of multiple concurrent computations.
Output
Result 1 Result 2 Result 3
5. Using CompletableFuture’s supplyAsync() Method to Transform a Future into a CompletableFuture
Another simple and effective way to convert a Future into a CompletableFuture is by using the supplyAsync() method of CompletableFuture. This method runs a task asynchronously in a specified executor or the common ForkJoinPool, allowing us to non-blockingly retrieve the result of the Future and complete the corresponding CompletableFuture.
Here’s a code example demonstrating how to use supplyAsync() to transform a Future into a CompletableFuture.
public class SupplyAsyncConversion {
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
Thread.sleep(2000);
return "Future Result";
});
// Convert Future to CompletableFuture using supplyAsync
CompletableFuture<String> completableFuture = transformUsingSupplyAsync(future, executor);
// Use CompletableFuture
completableFuture
.thenApply(result -> "Processed: " + result)
.thenAccept(System.out::println)
.exceptionally(ex -> {
System.out.println("Error: " + ex.getMessage());
return null;
});
executor.shutdown();
}
private static <T> CompletableFuture<T> transformUsingSupplyAsync(Future<T> future, Executor executor) {
return CompletableFuture.supplyAsync(() -> {
try {
return future.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}, executor);
}
}
In this approach, a Future is created using an ExecutorService to simulate an asynchronous computation. The supplyAsync() method is then used to non-blockingly execute a task that retrieves the result of the Future, running the task in the provided executor or the common ForkJoinPool by default.
The resulting CompletableFuture enables easy processing of the result using methods such as thenApply(), thenAccept(), and exceptionally(). Any exceptions encountered during the execution of the Future or while retrieving its result are rethrown and handled within the CompletableFuture.
6. Conclusion
In this article, we explored the limitations of Future and demonstrated how to transform it into CompletableFuture using background threads and polling. Additionally, we discussed how to combine multiple Future objects into a single CompletableFuture, enabling efficient management and aggregation of asynchronous tasks. These techniques help modernize legacy code, enhance readability, and improve the efficiency of concurrency in Java applications.
7. Download the Source Code
This article explored how to transform a Future into a CompletableFuture in Java.
You can download the full source code of this example here: transform java future to completablefuture


