Please, consider an example from the "Modern Java in Action" book (2nd Edition, listing 16.16, page 405). There, we have three map operation to obtain the list of discounted prices for the product from all the shops in a stream. First, we contact each shop to get a response containing non-discounted price along with a discount type, than parse the response into Quote object, and pass it over to the remote Discount service which returns a string with already discounted price.
public List<String> findPrices(String product) {
List<CompletableFuture<String>> priceFutures =
shops.stream()
.map(shop -> CompletableFuture.supplyAsync(
() -> shop.getPrice(product), executor))
.map(future -> future.thenApply(Quote::parse))
.map(future -> future.thenCompose(quote ->
CompletableFuture.supplyAsync(
() -> Discount.applyDiscount(quote), executor)))
.collect(toList());
return priceFutures.stream()
.map(CompletableFuture::join)
.collect(toList());
}
My question is not about the difference between thenApply
and thenCompose
. I believe, the latter is used to avoid nested construction like CompletableFuture<CompletableFuture<...>>
. What I don't understand, though, why we need to create another level of CompletableFuture
here at all? It seems like the author added some artificial complexity to the code by creating and then flattening nested CompletableFuture
, instead of simply using thenApplyAsync
in the third map like this:
.map(shop -> CompletableFuture.supplyAsync(
() -> shop.getPrice(product), executor))
.map(future -> future.thenApply(Quote::parse))
.map(future -> future.thenApplyAsync(Discount::applyDiscount, executor))
Are those two mapping usages (the original with thenCompose
and the one with thenApplyAsync
) equivalent? Both accept the result of the previous mapping as an argument, both provide custom executor to perform the task, and both return the same CompletableFuture<String>
result.