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.

Scroll to Top