CompletableFuture | thenApply vs thenCompose
Asked Answered
V

10

189

I can't get my head around the difference between thenApply and thenCompose.

So, could someone provide a valid use case?

From the Java docs:

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.

thenCompose(Function<? super T,? extends CompletionStage<U>> fn)

Returns a new CompletionStage that, when this stage completes normally, is executed with this stage as the argument to the supplied function.

I get that the 2nd argument of thenCompose extends the CompletionStage where thenApply does not.

Could someone provide an example in which case I have to use thenApply and when thenCompose?

Vaticinate answered 25/3, 2017 at 16:47 Comment(2)
Do you understand the difference between map and flatMap in Stream? thenApply is the map and thenCompose is the flatMap of CompletableFuture. You use thenCompose to avoid having CompletableFuture<CompletableFuture<..>>.Sporogony
This is a very nice guide to start with CompletableFuture - baeldung.com/java-completablefutureSupererogatory
C
253

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));
Carnotite answered 26/3, 2017 at 6:22 Comment(6)
Why should a programmer use .thenCompose(x -> CompletableFuture.supplyAsync(() -> x+1)) instead of .thenApplyAsync(x -> x+1)? Being synchronous or asynchronous is not the relevant difference.Difficult
They would not do so like that. However, if a third-party library that they used returned a CompletableFuture, then this would be where thenCompose would be the way to flatten the structure.Carnotite
@Difficult read my other answer if you're confused about thenApplyAsync because it is not what you think it is.Wiesbaden
@Wiesbaden I don’t know what makes you think that I was confused and there’s nothing in your answer backing your claim that “it is not what you think it is”.Difficult
@Difficult Apologies. I meant it might not be what you think it is. And you may read it if you're confused. I see mention of thenApplyAsync which doesn't have to do with the difference in this question. And my other answer is #47489838Wiesbaden
I honestly thing that a better code example that has BOTH sync and async functions with BOTH .supplyAsync().thenApply() and .supplyAsync(). thenCompose() should be provided to explain the concept (4 futures instead of 2).Witch
M
120

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.

Moustache answered 19/2, 2018 at 17:3 Comment(3)
Then Joe C's answer is not misleading. It is correct and more concise.Selsyn
Imho it is poor design to write CompletableFuture<UserInfo> getUserInfo and CompletableFuture<UserRating> getUserRating(UserInfo) \\ instead it should be UserInfo getUserInfo() and int getUserRating(UserInfo) if I want to use it async and chain, then I can use ompletableFuture.supplyAsync(x => getUserInfo(userId)).thenApply(userInfo => getUserRating(userInfo)) or anything like this, it is more readable imho, and not mandatory to wrap ALL return types into CompletableFutureChlorosis
@Chlorosis Whether it is poor design or not depends on whether the user rating is contained in the UserInfo (then yes) or whether it has to be obtained separately, maybe even costly (then no).Rumpf
S
51

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 and Stream.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 the CompletionStage 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, the CompletionStage 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 and Stream.flatMap.

See the CompletionStage documentation for rules covering exceptional completion.

Synergism answered 30/3, 2017 at 11:9 Comment(2)
I wonder why they didn't name those functions map and flatMap in the first place.Vainglorious
@MatthiasBraun I think that's because thenApply() will simply call Function.apply(), and thenCompose() is a bit similar to composing functions.Synergism
W
29

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.

Wiesbaden answered 27/7, 2018 at 15:41 Comment(1)
While i understand the example given, i think thenApply((y)->System.println(y)); doesnt work. It takes a function,but a consumer is given. We should replac it with thenAccept(y)->System.println(y))Cytaster
W
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
Whitton answered 29/11, 2020 at 11:32 Comment(1)
When I run your second code, it have same result System.out.println("Applying"+completableFutureToApply.get()); and System.out.println("Composing"+completableFutureToCompose.get()); , the comment at end of your post about time of execute task is right but the result of get() is same, can you explain the difference , thank youWadlinger
D
2

And if you are still confused about what makes the real difference in code when I use thenApply vs thenCompose and what a nested future looks like then please look at the full working example.

package com.graphql.demo.garphqlapi;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

public class ComposeVsThenApply {

    public static void main(String[] args) {
        ComposeVsThenApply cva = new ComposeVsThenApply();
        //thenCompose usage : Using the thenCompose for simplifying the return type of dependent processing.
        System.out.println("Starting thenCompose demo");
        CompletableFuture<StockRating> flattenedFuture = cva.getStockDetails("Apple").thenCompose((stock) -> {
            return cva.getRating(stock);
        });
        //Retrive results
        try {
            StockRating stockViaThenCompose = flattenedFuture.get();
            System.out.println("\n\t\tStock summery :" + stockViaThenCompose.getStockName() + ", Rating :" + stockViaThenCompose.getRating());
            System.out.println("--- thenCompose demo ended ---");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        //ThenAply: thenApply is good for result transformation but sucks when we have two asynchronous dependent processing. Now you get nested future.
        System.out.println("\n\n\nStarting thenApply demo");
        CompletableFuture<CompletableFuture<StockRating>> nestedFuture = cva.getStockDetails("Apple").thenApply((stock) -> {
            return cva.getRating(stock);
        });
        //Retrive results
        try {
            StockRating stockrViaThenApply = nestedFuture.get().get();
            System.out.println("\n\t\tStock summery :" + stockrViaThenApply.getStockName() + ", Rating :" + stockrViaThenApply.getRating());
            System.out.println("--- thenApply demo ended---");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    class Stock {
        private String name;

        public Stock(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }

    class StockRating {
        private Double rating;

        public boolean isBuyCall() {
            return buyCall;
        }

        public void setBuyCall(boolean buyCall) {
            this.buyCall = buyCall;
        }

        public String getStockName() {
            return stockName;
        }

        public void setStockName(String stockName) {
            this.stockName = stockName;
        }

        private boolean buyCall;

        public StockRating(Double rating, boolean buyCall, String stockName) {
            this.rating = rating;
            this.buyCall = buyCall;
            this.stockName = stockName;
        }

        private String stockName;

        public StockRating(Double rating) {
            this.rating = rating;
        }

        public Double getRating() {
            return rating;
        }

        public void setRating(Double rating) {
            this.rating = rating;
        }
    }

    class StockSupplier implements Supplier<Stock> {
        private String name;

        public StockSupplier(String name) {
            this.name = name;
        }

        @Override
        public Stock get() {
            try {
                System.out.println("\n\t\tRetriving details for " + this.name);
                TimeUnit.SECONDS.sleep(4);
                System.out.println("\n\t\tDone with details retrival for " + this.name);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return new Stock(name);
        }
    }

    class RatingSupplier implements Supplier<StockRating> {
        private Stock stock;

        public RatingSupplier(Stock stock) {
            this.stock = stock;
        }

        @Override
        public StockRating get() {
            try {
                System.out.println("\n\t\tRetriving stock rating for " + this.stock.getName());
                TimeUnit.SECONDS.sleep(4);
                System.out.println("\n\t\tDone with rating retrival for " + this.stock.getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return new StockRating(10d, true, this.stock.getName());
        }
    }

    public CompletableFuture<Stock> getStockDetails(String name) {
        return CompletableFuture.supplyAsync(new StockSupplier(name));
    }

    public CompletableFuture<StockRating> getRating(Stock stock) {
        return CompletableFuture.supplyAsync(new RatingSupplier(stock));
    }

    public String getSummeryReport(Stock stock, StockRating sr) {
        return stock.getName() + "/n " + sr.getRating();
    }
}
Dorri answered 27/12, 2021 at 7:14 Comment(0)
B
1

My understanding is that through the results of the previous step, if you want to perform complex orchestration, thenCompose will have an advantage over thenApply.

The following example is, through the results of the first step, go to two different places to calculate, whoever returns sooner, you can see the difference between them

    CompletableFuture<Integer> result = CompletableFuture.supplyAsync(() -> 1);
    // thenCompose
    System.out.println(result.thenCompose(i -> CompletableFuture.anyOf(CompletableFuture.supplyAsync(() -> i + 1), CompletableFuture.supplyAsync(() -> i + 2))).join());
    System.out.println(result.thenCompose(i -> CompletableFuture.supplyAsync(() -> i + 1).applyToEither(CompletableFuture.supplyAsync(() -> i + 2), j -> j)).join());
    // ----- thenApply
    System.out.println(result.thenApply(i -> CompletableFuture.anyOf(CompletableFuture.supplyAsync(() -> i + 1), CompletableFuture.supplyAsync(() -> i + 2)).join()).join());
    System.out.println(result.thenApply(i -> CompletableFuture.supplyAsync(() -> i + 1).applyToEither(CompletableFuture.supplyAsync(() -> i + 2), j -> j).join()).join());
Banka answered 28/12, 2021 at 8:47 Comment(0)
C
0

JoeC's answer is correct, but I think the better comparison that can clear the purpose of the thenCompose is the comparison between thenApply and thenApply! Once when a synchronous mapping is passed to it and once when an asynchronous mapping is passed to it.

If the mapping passed to the thenApply returns an String(a non-future, so the mapping is synchronous), then its result will be CompletableFuture<String>. Now similarly, what will be the result of the thenApply, when the mapping passed to the it returns a CompletableFuture<String>(a future, so the mapping is asynchronous)? The end result will be CompletableFuture<CompletableFuture<String>>, which is unnecessary nesting(future of future is still future!). Here's where we can use thenCompose to be able to "compose"(nest) multiple asynchronous tasks in each other without getting futures nested in the result.

Cactus answered 17/4, 2021 at 5:13 Comment(0)
A
0

private void test1() throws ExecutionException, InterruptedException {

    //thenApply返回的是之前的CompletableFuture
    CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 1)
            .thenApply((x) -> {
                x = x + 1;
                log.info("thenApply, 1, x:{}", x);
                return x;
            });

    System.out.println(future.get());
}

//thenCompose返回的是新的CompletableFuture
private void test2() throws ExecutionException, InterruptedException {
    CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 1)
            .thenCompose((x) -> {
                return CompletableFuture.supplyAsync(() -> {
                    Integer y = x + 1;
                    log.info("thenCompose, 1, x:{}", y);
                    return y;
                });
            });

    System.out.println(future.get());
}
Acea answered 16/11, 2021 at 8:32 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Mcnelly
D
-1
We can use this example suggested by @dorjee to understand the diff between thenApply and ThenCompose

here we're using ThenCompose cause userRatingCompletableFuture  is taking a completableFuture as an input. and if we call thenApply here it will return in a 


nested completableFuture of CompletableFuture<CompletableFuture<user>> instead of CompletableFuture<user>
  
{
        CompletableFuture<user> getUserInfo = new CompletableFuture<user>();
        CompletableFuture<String> getUserRating=new CompletableFuture<String>();

        user u=  new user();

        CompletableFuture<user> userCompletableFuture = getUserInfo.supplyAsync(() -> u);
        CompletableFuture<user> userRatingCompletableFuture = getUserRating.supplyAsync(()->userCompletableFuture).thenCompose(i-> {
            return CompletableFuture.supplyAsync(() -> {
                try {
                    return i.get();
                }
                catch (Exception ex) {
                throw new RuntimeException(ex);
            }
            });
        });
        System.out.println(userRatingCompletableFuture.get().id);
    }


}
class user{
    int id;
    String name;

    user(){
        id=100;
        name="john";
    }
}
Dashpot answered 11/2 at 10:53 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Mcnelly

© 2022 - 2024 — McMap. All rights reserved.