Core Java

CompletableFuture vs. Future in Java

Asynchronous programming is essential in modern Java applications for tasks like I/O operations, web service calls, or background computations. Java provides tools to handle asynchronous tasks, with Future and CompletableFuture being prominent options. While Future was introduced in Java 5, CompletableFuture in Java 8 added more powerful and flexible features for working with asynchronous computations. This article explores their differences, use cases, and why CompletableFuture often surpasses Future in flexibility and functionality.

java CompletableFuture

1. What is a Future?

A Future is an interface representing a placeholder for a result that will be available at some point in the future. It is primarily used with ExecutorService to perform asynchronous computations.

1.1 Key Features of Future:

  • Blocking Nature: You can use the get() method to retrieve the result, but it blocks until the computation is complete.
  • Limited Functionalities: Future provides basic methods like isDone() to check the task’s status, cancel() to stop execution, and get() to retrieve the result.

1.2 Example of Future:

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(() -> {
    Thread.sleep(2000); // Simulating a long-running task
    return 42;
});

System.out.println(future.get()); // Blocks until the result is available
executor.shutdown();

While useful, this approach is often cumbersome for complex workflows requiring chaining or combining tasks.

2. What is a CompletableFuture?

CompletableFuture, introduced in Java 8, is an extension of Future. It adds powerful methods to handle asynchronous programming with non-blocking and composable operations.

2.1 Key Features of CompletableFuture:

  1. Non-Blocking: Use thenApply, thenAccept, or thenRun to process results without blocking.
  2. Chaining: Combine multiple CompletableFutures using methods like thenCompose or thenCombine.
  3. Exception Handling: Built-in support for handling exceptions with methods like exceptionally.
  4. Manually Complete: CompletableFutures can be completed programmatically using complete() or completeExceptionally().

2.2 Example of CompletableFuture:

CompletableFuture.supplyAsync(() -> {
    return 42; // Simulating a computation
}).thenApply(result -> result * 2) // Transforming the result
  .thenAccept(System.out::println); // Printing the result

This example demonstrates a fully non-blocking approach to processing data.

3. Key Differences Between Future and CompletableFuture

FeatureFutureCompletableFuture
Blocking BehaviorBlocks with get()Non-blocking with callbacks
Chaining TasksNot supportedSupports chaining with then... methods
Combining TasksRequires manual effortEasily combine with thenCompose or thenCombine
Exception HandlingLimitedBuilt-in methods like exceptionally
Manual CompletionNot possibleSupported with complete()
Ease of UseBasicAdvanced and flexible

4. Use Cases

When to Use Future:

  • For simple asynchronous tasks where blocking is acceptable.
  • When integrating with legacy systems that rely on older concurrency APIs.

When to Use CompletableFuture:

  • For modern, scalable, and responsive applications.
  • When dealing with multiple asynchronous tasks that require chaining or combining results.
  • For handling exceptions elegantly in an asynchronous workflow.

5. Advanced CompletableFuture Features

  1. Combining Multiple Futures
    You can run multiple tasks and combine their results seamlessly:
CompletableFuture<Integer> task1 = CompletableFuture.supplyAsync(() -> 50);
CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(() -> 70);

task1.thenCombine(task2, Integer::sum)
     .thenAccept(result -> System.out.println("Sum: " + result));

2. Handling Exceptions
Gracefully handle exceptions without crashing the application:

CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("Error occurred");
}).exceptionally(ex -> {
    System.out.println("Handled Exception: " + ex.getMessage());
    return 0;
}).thenAccept(System.out::println);

3. Timeout Handling
Avoid indefinitely waiting for a result:

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    Thread.sleep(5000);
    return 42;
});
future.orTimeout(2, TimeUnit.SECONDS)
      .exceptionally(ex -> {
          System.out.println("Timeout occurred");
          return -1;
      })
      .thenAccept(System.out::println);

6. Conclusion

While Future served its purpose as a foundational abstraction for asynchronous programming in Java, CompletableFuture significantly enhances the developer’s ability to handle asynchronous workflows with non-blocking, composable, and flexible methods. For most modern applications, CompletableFuture is the preferred choice due to its rich API and capabilities, making asynchronous programming in Java both powerful and convenient.

For developers transitioning to asynchronous patterns, understanding and leveraging CompletableFuture is an essential step towards building scalable and efficient applications. Whether you’re chaining tasks, handling exceptions, or combining workflows, CompletableFuture provides the tools to meet the demands of today’s reactive systems.

Eleftheria Drosopoulou

Eleftheria is an Experienced Business Analyst with a robust background in the computer software industry. Proficient in Computer Software Training, Digital Marketing, HTML Scripting, and Microsoft Office, they bring a wealth of technical skills to the table. Additionally, she has a love for writing articles on various tech subjects, showcasing a talent for translating complex concepts into accessible content.
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