Core Java

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

Example screenshot showing the output of transforming a Java Future into a CompletableFuture
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.

Download
You can download the full source code of this example here: transform java future to completablefuture

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest


This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button