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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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
1 | 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:
Future
doesn’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.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | 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
1 | 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.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 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
1 | 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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | 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
1 2 3 | 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
.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | 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