Blog Concurrency and Multithreading Java Java 8 Features

Differences between the CompletableFuture methods in terms of their functionality?

In Java, CompletableFuture is a class that provides a way to perform asynchronous computations and handle their results. It supports various methods that allow you to chain multiple asynchronous operations together and perform actions when the computations are completed.

Here’s an explanation of some of the important methods provided by CompletableFuture with examples:

  1. supplyAsync(): This method takes a Supplier Function and returns a new CompletableFuture that will be completed with the result of a Supplier function executed asynchronously.

    It allows you to start an asynchronous computation and obtain its result as a CompletableFuture.

Example:

Java
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // Simulate a long-running task
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "Hello, World!";
});

future.thenAccept(result -> System.out.println(result));
// Output: Hello, World!

  1. thenApply(): This method is used to transform the result of a CompletableFuture by applying a Function to its value.

    It returns a new CompletableFuture that will be completed with the result of applying the provided Function to the original CompletableFuture‘s result.

Java
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn) {
     return uniApplyStage(null, fn);
}

Example:

Java
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello")
        .thenApply(str -> str + ", World!");

future.thenAccept(result -> System.out.println(result));
// Output: Hello, World!
  1. thenAccept(): This method is used to perform a side-effect action on the result of a CompletableFuture without transforming the value.

    It takes a Consumer that consumes the result of the CompletableFuture but does not return a new value. The original CompletableFuture is returned, allowing you to chain further operations if needed.
Java
 public CompletableFuture<Void> thenAccept(Consumer<? super T> action) {
        return uniAccept
}

Example:

Java
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello, World!");

future.thenAccept(result -> System.out.println(result));
// Output: Hello, World!
  1. thenCompose(): This method takes a Function that returns a CompletableFuture and returns a new CompletableFuture that will be completed with the result of the returned CompletableFuture.
Java
 public <U> CompletableFuture<U> thenCompose(
        Function<? super T, ? extends CompletionStage<U>> fn) {
        return uniComposeStage(null, fn);
    }

Example:

Java
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello")
        .thenCompose(str -> CompletableFuture.supplyAsync(() -> str + ", World!"));

future.thenAccept(result -> System.out.println(result));
// Output: Hello, World!
  1. thenCombine(): This method takes another CompletableFuture and a BiFunction that combines the results of both futures. It returns a new CompletableFuture that will be completed with the result of applying the BiFunction to the results of the two input futures.

Example:

Java
CompletableFuture<String> hello = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> world = CompletableFuture.supplyAsync(() -> "World");

CompletableFuture<String> future = hello.thenCombine(world, (str1, str2) -> str1 + ", " + str2 + "!");

future.thenAccept(result -> System.out.println(result));
// Output: Hello, World!

Practical Example that Showcases the usage of various CompletableFuture methods

1. Kick-starting Asynchronous Computations with supplyAsync()

Fetch data from API 1 using a GET request

Java
// Step 1: Fetch data from API 1 using a GET request
        CompletableFuture<String> dataFuture1 = CompletableFuture.supplyAsync(() -> {
            try {
                URL url = new URL("https://api.example.com/data1");
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                connection.setRequestMethod("GET");
                // ... (handle response and return data1)
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        });
        

Fetch data from API 2 using a POST request

Java
// Step 2: Fetch data from API 2 using a POST request
        CompletableFuture<String> dataFuture2 = CompletableFuture.supplyAsync(() -> {
            try {
                URL url = new URL("https://api.example.com/data2");
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                connection.setRequestMethod("POST");
                // ... (handle request body and response, return data2)
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        });
        

In these steps, the lambda expressions passed to supplyAsync() act as Supplier functions. They encapsulate the logic for fetching data from the respective APIs and return the fetched data.

2. Combining Results with thenCombine()

Java
// Step 3: Combine the results from API 1 and API 2
        CompletableFuture<String> combinedDataFuture = dataFuture1.thenCombine(dataFuture2, (data1, data2) -> {
            String combinedData = "Data1: " + data1 + ", Data2: " + data2;
            return combinedData;
        });

In this step, the lambda expression (data1, data2) -> { ... } is a BiFunction that takes the results of the two CompletableFuture instances (data1 and data2) and combines them into a new result (combinedData). This BiFunction is passed as an argument to the thenCombine() method.

3. Transforming Data with thenApply()

Java
// Step 4: Transform the combined data
        CompletableFuture<String> transformedDataFuture = combinedDataFuture.thenApply(combinedData -> {
            String transformedData = combinedData.toUpperCase();
            return transformedData;
        });

In this step, the lambda expression combinedData -> { ... } is a Function that takes the combined data (combinedData) and transforms it by converting it to uppercase, returning the transformed data (transformedData). This Function is passed as an argument to the thenApply() method.

4. Chaining Asynchronous Operations with thenCompose()

Java
// Step 5: Use the transformed data to make another API call
        CompletableFuture<String> finalDataFuture = transformedDataFuture.thenCompose(transformedData -> {
            return CompletableFuture.supplyAsync(() -> {
                try {
                    URL url = new URL("https://api.example.com/final");
                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                    connection.setRequestMethod("PUT");
                    // ... (handle request body using transformedData, return finalData)
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return null;
            });
        });

In this step, the lambda expression transformedData -> { ... } is a Function that takes the transformed data (transformedData) and returns a new CompletableFuture instance.

Inside this Function, we use supplyAsync() to initiate another asynchronous operation (making a PUT request to the third API). The lambda expression passed to supplyAsync() acts as a Supplier function, encapsulating the logic for making the API call and returning the final data.

5. Consuming the Final Result with thenAccept()

Java
 // Step 6: Consume the final data
        finalDataFuture.thenAccept(finalData -> {
            if (finalData != null) {
                System.out.println("Final data: " + finalData);
                // ... (perform additional operations with the final data)
            }
        });

In this final step, the lambda expression finalData -> { ... } is a Consumer that takes the final data (finalData) and performs side-effect operations, such as printing the final data or performing additional operations with it. This Consumer is passed as an argument to the thenAccept() method.

You can read more detail on

Ref: https://codetechsummit.com/completablefuture-methods-differences/

Avatar

Neelabh

About Author

As Neelabh Singh, I am a Senior Software Engineer with 6.6 years of experience, specializing in Java technologies, Microservices, AWS, Algorithms, and Data Structures. I am also a technology blogger and an active participant in several online coding communities.

You may also like

Blog Design Pattern

Understanding the Builder Design Pattern in Java | Creational Design Patterns | CodeTechSummit

Overview The Builder design pattern is a creational pattern used to construct a complex object step by step. It separates
Blog Tech Toolkit

Base64 Decode

Base64 encoding is a technique used to encode binary data into ASCII characters, making it easier to transmit data over