CompletableFuture | thenApply vs thenCompose
Solution 1:
thenApply
is used if you have a synchronous mapping function.
CompletableFuture<Integer> future =
CompletableFuture.supplyAsync(() -> 1)
.thenApply(x -> x+1);
thenCompose
is used if you have an asynchronous mapping function (i.e. one that returns a CompletableFuture
). It will then return a future with the result directly, rather than a nested future.
CompletableFuture<Integer> future =
CompletableFuture.supplyAsync(() -> 1)
.thenCompose(x -> CompletableFuture.supplyAsync(() -> x+1));
Solution 2:
I think the answered posted by @Joe C is misleading.
Let me try to explain the difference between thenApply
and thenCompose
with an example.
Let's suppose that we have 2 methods: getUserInfo(int userId)
and getUserRating(UserInfo userInfo)
:
public CompletableFuture<UserInfo> getUserInfo(userId)
public CompletableFuture<UserRating> getUserRating(UserInfo)
Both method return types are CompletableFuture
.
We want to call getUserInfo()
first, and on its completion, call getUserRating()
with the resulting UserInfo
.
On the completion of getUserInfo()
method, let's try both thenApply
and thenCompose
. The difference is in the return types:
CompletableFuture<CompletableFuture<UserRating>> f =
userInfo.thenApply(this::getUserRating);
CompletableFuture<UserRating> relevanceFuture =
userInfo.thenCompose(this::getUserRating);
thenCompose()
works like Scala's flatMap
which flattens nested futures.
thenApply()
returned the nested futures as they were, but thenCompose()
flattened the nested CompletableFutures
so that it is easier to chain more method calls to it.
Solution 3:
The updated Javadocs in Java 9 will probably help understand it better:
thenApply
<U> CompletionStage<U> thenApply(Function<? super T,? extends U> fn)
Returns a new
CompletionStage
that, when this stage completes normally, is executed with this stage's result as the argument to the supplied function.This method is analogous to
Optional.map
andStream.map
.See the
CompletionStage
documentation for rules covering exceptional completion.
thenCompose
<U> CompletionStage<U> thenCompose(Function<? super T,? extends CompletionStage<U>> fn)
Returns a new
CompletionStage
that is completed with the same value as theCompletionStage
returned by the given function.When this stage completes normally, the given function is invoked with this stage's result as the argument, returning another
CompletionStage
. When that stage completes normally, theCompletionStage
returned by this method is completed with the same value.To ensure progress, the supplied function must arrange eventual completion of its result.
This method is analogous to
Optional.flatMap
andStream.flatMap
.See the
CompletionStage
documentation for rules covering exceptional completion.
Solution 4:
thenApply
and thenCompose
are methods of CompletableFuture
. Use them when you intend to do something to CompletableFuture
's result with a Function
.
thenApply
and thenCompose
both return a CompletableFuture
as their own result. You can chain multiple thenApply
or thenCompose
together. Supply a Function
to each call, whose result will be the input to the next Function
.
The Function
you supplied sometimes needs to do something synchronously. The return type of your Function
should be a non-Future
type. In this case you should use thenApply
.
CompletableFuture.completedFuture(1)
.thenApply((x)->x+1) // adding one to the result synchronously, returns int
.thenApply((y)->System.println(y)); // value of y is 1 + 1 = 2
Other times you may want to do asynchronous processing in this Function
. In that case you should use thenCompose
. The return type of your Function
should be a CompletionStage
. The next Function
in the chain will get the result of that CompletionStage
as input, thus unwrapping the CompletionStage
.
// addOneAsync may be implemented by using another thread, or calling a remote method
abstract CompletableFuture<Integer> addOneAsync(int input);
CompletableFuture.completedFuture(1)
.thenCompose((x)->addOneAsync(x)) // doing something asynchronous, returns CompletableFuture<Integer>
.thenApply((y)->System.println(y)); // y is an Integer, the result of CompletableFuture<Integer> above
This is a similar idea to Javascript's Promise
. Promise.then
can accept a function that either returns a value or a Promise
of a value. The reason why these two methods have different names in Java is due to generic erasure. Function<? super T,? extends U> fn
and Function<? super T,? extends CompletionStage<U>> fn
are considered the same Runtime type - Function
. Thus thenApply
and thenCompose
have to be distinctly named, or Java compiler would complain about identical method signatures. The end result being, Javascript's Promise.then
is implemented in two parts - thenApply
and thenCompose
- in Java.
You can read my other answer if you are also confused about a related function thenApplyAsync
.
Solution 5:
thenCompose() is better for chaining CompletableFuture.
thenApply() is better for transform result of Completable future.
You can achieve your goal using both techniques, but one is more suitable for one use case then other.
public CompletableFuture<Integer> process(Integer i) {
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(
() -> new HeavyTask(i).execute());
return completableFuture;
}
@SneakyThrows
public CompletableFuture<Integer> thenApplyVsThenCompose() {
// each time calling thenApply() function returns CompletionState
// so you will get nested Futures
// you can think about it like map() java optional
CompletableFuture<Future<Integer>> cf1 = CompletableFuture.supplyAsync(
() -> new HeavyTask().execute())
.thenApply(i -> process(i));
// to get result you will have to get nested get() calls
Integer resultFromThenApply = cf1.get().get();
// when calling thenCompose() nested Futures are flatten
// you can think about it like flatMap() java optional
CompletableFuture<Integer> cf2;
cf2 = CompletableFuture.supplyAsync(
() -> new HeavyTask().execute())
.thenCompose(this::process);
// you have to just call one get() since thenCompose was flatten
Integer resultFromThenCompose = cf2.get();
return null;
}
Other problem that can visualize difference between those two
How would you implement solution when you do not know how many time you have to apply thenApply()/thenCompose() (in case for example recursive methods)?
public void nested() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> completableFutureToCompose = CompletableFuture.completedFuture(1);
for (int i = 0; i < 10; i++) {
log.info("Composing");
completableFutureToCompose = completableFutureToCompose.thenCompose(this::process);
}
completableFutureToCompose.get();
// not achievable using then apply
CompletableFuture<Integer> completableFutureToApply = CompletableFuture.completedFuture(1);
for (int i = 0; i < 10; i++) {
log.info("Applying");
completableFutureToApply = completableFutureToApply.thenApply(this::process).get();
}
completableFutureToCompose.get();
}
public CompletableFuture<Integer> process(Integer i) {
log.info("PROCESSING");
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(
() -> new HeavyTask(i).execute());
return completableFuture;
}
- Using composing you first create receipe how futures are passed one to other and then execute
- Using apply you execute logic after each apply invocation