Blog

Mastering the Potential of Java’s CompletableFuture

Java 8 ushered in a plethora of novel functionalities, including the groundbreaking Streams API and the versatile Lambda expressions. Amidst these, one standout inclusion was the CompletableFuture. In the forthcoming article, we shall delve into the world of CompletableFuture in Java, exploring its extensive repertoire of methods. Our aim is to demystify its inner workings and provide you with practical, easy-to-follow examples on how to seamlessly integrate it into your Java code. 

What is CompletableFuture? 

 

In the world of Java programming, CompletableFuture shines as a powerful feature for asynchronous coding. In contrast to the conventional procedural approach, asynchronous programming is a paradigm that unleashes the true potential of your applications. How? By executing tasks on separate threads, freeing your main application thread to keep tabs on task progress, completion status, and potential hiccups. This dynamic approach ensures that your main thread remains unblocked, capable of running in parallel with other tasks, thus significantly elevating your program’s performance and slashing execution times. 

Why CompletableFuture Matters: A Deeper Dive into Java’s Asynchronous Landscape 

 

You may have crossed paths with the Future API in Java, a precursor to CompletableFuture, which made its debut way back in Java 5. The Future API was initially designed to serve as a reference to the outcomes of asynchronous computations—a stepping stone towards more efficient and responsive Java applications. It featured methods like isDone() to check if a computation had concluded and get() to retrieve the result once it had finished. 

 

Future vs. CompletableFuture: Unmasking the Titans 

 

Future API: A Glimpse into the Past 

 

While Future API was a commendable stride towards asynchronous programming in Java, it had its limitations. Here’s where CompletableFuture swoops in, offering a suite of capabilities that were sorely lacking in its predecessor:

 

CompletableFuture API: The Future is Now

 

Manual Completion:

 

  • CompletableFuture allows manual completion. In situations involving remote APIs, you can manually complete the Future to retrieve data even if the remote service encounters downtime;
  • Future API, on the other hand, does not grant this flexibility. A hiccough in a remote API server could bring your program to a screeching halt, rendering it unresponsive.

 

Callback Functions:

 

  • CompletableFuture empowers you with callback functions that are automatically invoked when results become available. This paves the way for subsequent actions to be executed seamlessly;
  • In contrast, Future provides no such notifications of completion. It merely offers the get() method, which blocks until the result surfaces, curtailing your ability to take further actions in response.

 

Asynchronous Workflow:

 

  • CompletableFuture facilitates the creation of intricate asynchronous workflows. It allows you to chain multiple APIs, sending one result to another in a streamlined manner;
  • The Future API, on the other hand, requires manual intervention for chaining multiple Future APIs.

 

Combining Multiple Futures:

 

  • CompletableFuture permits the combination of multiple futures. This means you can run multiple APIs in parallel and then merge them with functions once they all complete;
  • While combining multiple futures is conceivable with Future APIs, you must handle the post-processing manually.

 

Exception Handling:

 

  • CompletableFuture shines with robust exception handling. By implementing the Future and CompletionStage interfaces, it provides a comprehensive set of methods for streamlined error management.

 

In contrast, Future API lacked any built-in mechanism for handling exceptions, leaving developers grappling with error scenarios.

 

Constructing a CompletableFuture: Diving into the Fundamentals

 

Creating a CompletableFuture is foundational when managing asynchronous programming in Java. The CompletableFuture object can be effortlessly constructed using a non-parameterized constructor, as demonstrated below:

 

CompletableFuture<String> initiatedCompletableFuture = new CompletableFuture<>();

 

This instantiation denotes the most fundamental method to establish a CompletableFuture. It serves as a cornerstone for handling future computations, representing an eventual completion or failure of computation tasks.

 

Client Interaction with CompletableFuture

 

For clients wishing to retrieve the result of this CompletableFuture, invoking the get() method is essential, as demonstrated by the following:

 

String retrievedResult = initiatedCompletableFuture.get();

 

It is pivotal to recognize that the get() method imposes a blocking call, meaning it will wait indefinitely until the CompletableFuture has reached completion. Thus, if the CompletableFuture remains incomplete indefinitely, the mentioned call will persist in its blocked state, causing potential deadlock situations in application processes.

 

Ensuring Completion of CompletableFuture

 

To circumvent indefinite blocking, utilizing the complete() method allows for manual completion of a CompletableFuture, illustrated as follows:

 

initiatedCompletableFuture.complete(“Computed Result”);

 

Clients in wait for this CompletableFuture will be notified of the specified result. Any subsequent invocations to initiatedCompletableFuture.complete() will be disregarded, preventing redundancy and maintaining the integrity of the process. This manual completion is particularly useful in situations where external conditions or computations determine the completion of the task, facilitating enhanced control over asynchronous task management.

 

Delving Deeper into CompletableFuture Interaction

 

Dynamic Interaction with Multiple Clients

 

The interaction between CompletableFuture and various clients is noteworthy. Multiple clients can eagerly await the result of a CompletableFuture, illustrating the flexibility and dynamic nature of this construct in managing concurrent processes and ensuring the seamless retrieval of computation results or handling of computation failures.

 

Enhanced Flexibility through Non-Blocking Calls

 

To further enrich the flexibility in handling CompletableFuture, employing non-blocking calls via methods like thenAccept() and thenApply() can be explored. These methods facilitate the execution of subsequent actions upon the completion of the original CompletableFuture, enhancing the responsiveness and adaptability of applications.

 

Comprehensive Exception Handling

 

A more refined approach to using CompletableFuture also involves comprehensive exception handling mechanisms. These mechanisms permit the detection and handling of any computation failures, ensuring robustness and resilience in the asynchronous programming landscape. Leveraging methods like exceptionally() and handle(), allows for meticulous management of exceptions, bolstering the reliability of asynchronous tasks.

 

Harnessing the Power of CompletableFuture: Running Asynchronous Computations

 

1. CompletableFuture.runAsync() – Lightning-Fast Background Tasks

 

Sometimes, you need to perform a background task without expecting any value in return. This is where CompletableFuture.runAsync() comes into play. It allows you to run tasks asynchronously, freeing up your main thread for other critical operations. Here’s how it works:

 

  • Method Signature: CompletableFuture<Void> CompletableFuture.runAsync(Runnable runnable);
  • Runnable Object: Create a Runnable object that encapsulates the task you want to execute in the background;
  • Execution: The specified task runs asynchronously in a separate thread, allowing your main thread to continue its work;
  • Blocking: You can choose to block and wait for the future to complete using future.get() if necessary.

 

Recommendations and Tips:

 

  • Use runAsync() for tasks that don’t need to return any results but must execute independently;
  • Employ it for tasks like logging, monitoring, or triggering events in the background.

 

2. CompletableFuture.supplyAsync() – Retrieving Asynchronous Results

 

What if your background task yields a valuable result that you need to access later? This is where CompletableFuture.supplyAsync() shines. It enables you to run tasks asynchronously and return their results as CompletableFuture<T>. Let’s break it down:

 

  • Method Signature: CompletableFuture<T> CompletableFuture.supplyAsync(Supplier<T> supplier);
  • Supplier Object: Craft a Supplier<T> object that defines the task and its return value;
  • Execution: The task executes asynchronously, and the result is wrapped in a CompletableFuture<T>;
  • Result Retrieval: You can retrieve the result using future.get() when you’re ready.

 

Insights and Best Practices:

 

  • Use supplyAsync() when you need the result of your background task.
  • It’s ideal for tasks like fetching data from external sources, making API calls, or performing calculations in parallel.

 

3. Supplier<T> – A Simple Yet Powerful Functional Interface

 

To fully grasp supplyAsync(), let’s explore the core of its functionality – the Supplier<T> interface:

 

  • What is a Supplier<T>?: It’s a functional interface that serves as a supplier of results;
  • Single Method – get(): Supplier<T> has a single method, get(), where you define your background task and specify the result to be supplied;
  • Versatility: Use Supplier<T> for various tasks where you need to produce a result, making your code more modular and concise;
  • Example: In our example, the Supplier<String> encapsulates a background task that sleeps for 3 seconds and then returns “Result of the asynchronous computation.”

 

By harnessing the power of CompletableFuture along with the versatility of Supplier<T>, you can create responsive and efficient Java applications that excel in handling asynchronous operations. Whether you need to run tasks without expecting a return value or retrieve valuable results, CompletableFuture empowers you to do so with ease and efficiency.

 

Transforming and Leveraging CompletableFuture for Asynchronous Operations

 

Asynchronous programming has become indispensable in modern software development, offering enhanced efficiency and responsiveness. One key element in crafting robust asynchronous systems is understanding how to manipulate and respond to the results of asynchronous operations. In Java, CompletableFuture is a potent tool for this purpose. This guide will delve into how to transform and act upon a CompletableFuture, providing you with valuable insights and methods to harness its full potential.

 

Attaching Callbacks: A Crucial Step in CompletableFuture Handling

 

When working with CompletableFuture, attaching a callback is essential. A callback is a piece of code that gets executed automatically once the CompletableFuture completes. By attaching callbacks, you can seamlessly process and manipulate the result when it becomes available.

 

Here are two essential methods for attaching callbacks:

 

thenApply() – Transforming the Result

 

  • The thenApply() method is your go-to choice when you want to process and transform the result of a CompletableFuture as soon as it arrives;
  • It takes a Function<T, R> as an argument, where T represents the input type and R represents the output type.

 

Example:

 

// Creating a CompletableFuture

CompletableFuture<String> firstNameFuture = CompletableFuture.supplyAsync(() -> {

    try {

        TimeUnit.SECONDS.sleep(3);

    } catch (InterruptedException e) {

        throw new IllegalStateException(e);

    }

    return “Shaharyar”;

});

 

// Attaching a callback to the Future using thenApply()

CompletableFuture<String> greetingFuture = firstNameFuture.thenApply(firstName -> {

    return firstName + ” Lalani”;

});

 

// Block and get the result of the future.

System.out.println(greetingFuture.get()); // Shaharyar Lalani

 

Tips:

 

You can chain a sequence of transformations by attaching multiple thenApply() callback methods. The result of one thenApply() method will be passed to the next in the series, enabling you to perform complex data transformations.

 

thenAccept() and thenRun() – Non-Transformative Callbacks

 

If you need to execute code after the Future is executed without returning a new value, you can use thenAccept() and thenRun() methods.

 

These methods are consumers and are typically used as the last callbacks in the chain.

thenAccept() – Consuming the Result

 

thenAccept() takes a Consumer<T> as an argument and returns a CompletableFuture<Void>.

 

Example:

 

// thenAccept() demo

CompletableFuture.supplyAsync(() -> {

    return EventBooking.getPricingDetail(eventId);

}).thenAccept(event -> {

    System.out.println(“Got the ticket prices of ” + event.getName());

});

thenRun() – Executing Code

 

thenRun() does not have access to the CompletableFuture results and takes a Runnable as its argument, returning CompletableFuture<Void>.

 

Example:

 

// thenRun() demo

CompletableFuture.supplyAsync(() -> {

    // Running the computation

}).thenRun(() -> {

    // Computation done.

});

 

Insight:

 

Use thenAccept() when you want to process the result, and thenRun() when you need to run non-result-dependent code, such as logging or cleanup.

 

Merging Dual CompletableFutures for Enhanced Data Retrieval

 

To retrieve and manipulate data from distinct services in a seamless manner, Java provides the capability to merge two CompletableFutures, using a method called thenCompose(). This technique can be highly valuable when there is a need to fetch data from one service and leverage this data to procure additional related information from another service.

Example of completablefuture in Java

Conceptual Implementation: Fetching Detailed Events and Available Tickets

 

To conceptualize, one can imagine having methods like getEventDetail() and getTicketsAvailable() which are implemented as follows:

 

CompletableFuture<User> getEventDetail(String eventId) {

   return CompletableFuture.supplyAsync(() -> {

      return EventBooking.getPricingDetail(eventId);

   });

}

 

CompletableFuture<Double> getTicketsAvailable(Event event) {

   return CompletableFuture.supplyAsync(() -> {

      return EventTickets.getTicketsAvailable(event);

   });

}

 

Here, getEventDetail() assembles details about an event, including its pricing, by making asynchronous requests, possibly to a remote API. The getTicketsAvailable() method, meanwhile, computes the available tickets for a specific event, likely by interacting with another service. These methods return a CompletableFuture representing the completion of these asynchronous computations.

 

Using thenCompose() to Integrate Results from Dual Futures

 

To combine the results from the above-mentioned Futures, developers can utilize the thenCompose() method as exemplified below:

 

CompletableFuture<CompletableFuture<Double>> result = 

getEventDetail(eventId)

.thenCompose(event -> getTicketsAvailable(event));

 

In this scenario, thenCompose() becomes vital as it allows the collation of results from two CompletableFutures into a single unified entity. Unlike the thenApply() method, which is used when the output isn’t another CompletableFuture, thenCompose() is indispensable when dealing with nested CompletableFuture instances.

 

Realizing Top-Level Future with thenCompose()

 

The example provided elucidates the importance of thenCompose() in ensuring that the final output is a top-level Future, avoiding unnecessary nested structures which could complicate data retrieval and processing. This method mitigates the complexity arising from the involvement of multiple CompletableFutures, allowing a streamlined approach to obtaining and managing data from diverse sources.

 

Incorporating Independent Futures Efficiently

 

Furthermore, thenCompose() is not restricted to cases where one Future is dependent on the other. It’s also possible and quite efficient to blend two independent Futures, providing a cohesive and organized means to synchronize multiple asynchronous tasks and capitalize on the results concurrently.

 

Conclusion

 

In conclusion, CompletableFuture is a powerful and versatile feature introduced in Java that has revolutionized asynchronous programming. Through its elegant design and rich set of methods, CompletableFuture simplifies the creation of concurrent, non-blocking code and provides a more intuitive way to manage complex asynchronous operations.

 

In this article, we’ve explored the fundamentals of CompletableFuture, from its creation to various combinators and exception handling techniques. We’ve seen how it can be used to orchestrate multiple asynchronous tasks, making it easier to build responsive and efficient applications.

No Comments

Sorry, the comment form is closed at this time.