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:
supplyAsync()
: This method takes a Supplier Function and returns a newCompletableFuture
that will be completed with the result of aSupplier
function executed asynchronously.
It allows you to start an asynchronous computation and obtain its result as aCompletableFuture
.
Example:
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!
thenApply()
: This method is used to transform the result of aCompletableFuture
by applying aFunction
to its value.
It returns a newCompletableFuture
that will be completed with the result of applying the providedFunction
to the originalCompletableFuture
‘s result.
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn) {
return uniApplyStage(null, fn);
}
Example:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello")
.thenApply(str -> str + ", World!");
future.thenAccept(result -> System.out.println(result));
// Output: Hello, World!
thenAccept()
: This method is used to perform a side-effect action on the result of aCompletableFuture
without transforming the value.
It takes aConsumer
that consumes the result of theCompletableFuture
but does not return a new value. The originalCompletableFuture
is returned, allowing you to chain further operations if needed.
public CompletableFuture<Void> thenAccept(Consumer<? super T> action) {
return uniAccept
}
Example:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello, World!");
future.thenAccept(result -> System.out.println(result));
// Output: Hello, World!
thenCompose()
: This method takes aFunction
that returns aCompletableFuture
and returns a newCompletableFuture
that will be completed with the result of the returnedCompletableFuture
.
public <U> CompletableFuture<U> thenCompose(
Function<? super T, ? extends CompletionStage<U>> fn) {
return uniComposeStage(null, fn);
}
Example:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello")
.thenCompose(str -> CompletableFuture.supplyAsync(() -> str + ", World!"));
future.thenAccept(result -> System.out.println(result));
// Output: Hello, World!
thenCombine()
: This method takes anotherCompletableFuture
and aBiFunction
that combines the results of both futures. It returns a newCompletableFuture
that will be completed with the result of applying theBiFunction
to the results of the two input futures.
Example:
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
// 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
// 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()
// 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()
// 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()
// 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()
// 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/