У чому різниця між thenApply() і thenCompose()
Обидва методи використовуються для ланцюжків CompletableFuture, але відрізняються тим, що повертають:
🟢 Junior Level
Обидва методи використовуються для ланцюжків CompletableFuture, але відрізняються тим, що повертають:
thenApply()— приймає функціюT -> U, повертаєCompletableFuture<U>thenCompose()— приймає функціюT -> CompletableFuture<U>, повертаєCompletableFuture<U>
// thenApply — проста трансформація
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> "Hello");
cf.thenApply(s -> s.length()) // String -> Integer
.thenAccept(len -> System.out.println(len)); // 5
// thenCompose — коли функція вже повертає CompletableFuture
CompletableFuture<String> cf = getUserIdAsync();
cf.thenCompose(userId -> getUserDetailsAsync(userId)) // String -> CompletableFuture<User>
.thenAccept(user -> System.out.println(user.name()));
Проста аналогія:
thenApply— якmapу Stream: трансформує значенняthenCompose— якflatMapу Stream: розкриває вкладений CompletableFuture
🟡 Middle Level
Детальне порівняння
thenApply — для синхронних трансформацій:
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> "Hello");
// Функція: String -> Integer (синхронна)
cf.thenApply(String::length) // CompletableFuture<Integer>
.thenApply(len -> len * 2) // CompletableFuture<Integer>
.thenAccept(System.out::println); // 10
thenCompose — для асинхронних операцій:
CompletableFuture<String> cf = getUserIdAsync();
// Функція: String -> CompletableFuture<User> (асинхронна)
cf.thenCompose(userId -> getUserAsync(userId)) // CompletableFuture<User>
.thenCompose(user -> getOrdersAsync(user.id())) // CompletableFuture<List<Order>>
.thenAccept(orders -> System.out.println(orders.size()));
Типові помилки
- Вкладені CompletableFuture:
```java
// ❌ thenApply створює вкладений CompletableFuture
CompletableFuture
userId = getUserIdAsync(); CompletableFuture<CompletableFuture > nested = userId.thenApply(id -> getUserAsync(id)); // Nested!
// ✅ thenCompose розкриває вкладеність
CompletableFuture
2. **Плутанина коли що використовувати:**
```java
// thenApply — коли результат синхронний
cf.thenApply(s -> s.toUpperCase());
// thenCompose — коли результат асинхронний
cf.thenCompose(id -> fetchDataFromApi(id));
🔴 Senior Level
Internal Implementation
thenApply:
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn) {
return uniApplyStage(null, fn);
}
// Створює новий CompletableFuture з UniApply залежністю
// Коли цей CF завершується — застосовується функція fn
// Результат fn встановлюється в новий CF
thenCompose:
public <U> CompletableFuture<U> thenCompose(
Function<? super T, ? extends CompletionStage<U>> fn
) {
return uniComposeStage(null, fn);
}
// Створює новий CompletableFuture з UniCompose залежністю
// Коли цей CF завершується — викликається fn (повертає CompletionStage)
// Результат fn "розкривається" в новий CF
Архітектурні Trade-offs
| thenApply | thenCompose |
|---|---|
| Синхронна функція | Асинхронна функція |
| T -> U | T -> CompletableFuture |
| Як map | Як flatMap |
| Немає вкладеності | Розкриває вкладеність |
Edge Cases
1. Ланцюжок thenCompose:
// Типовий async workflow
getUserIdAsync()
.thenCompose(userId -> getUserAsync(userId))
.thenCompose(user -> getOrdersAsync(user.id()))
.thenCompose(orders ->
orders.isEmpty()
? CompletableFuture.completedFuture(List.<OrderDetail>of())
: getOrderDetailsAsync(orders.get(0).id())
)
.thenAccept(details -> process(details));
2. Exception propagation:
// Виключення в thenApply:
cf.thenApply(s -> { throw new RuntimeException("Error"); })
.exceptionally(ex -> "recovered"); // спіймає
// Виключення в composed CF:
cf.thenCompose(id ->
getUserAsync(id) // може викинути
.exceptionally(ex -> defaultUser) // обробка всередині
)
.exceptionally(ex -> fallback); // обробка ззовні
Продуктивність
thenApply:
- Overhead: ~5 ns (застосування функції)
- No thread switch
thenCompose:
- Overhead: ~10 ns (розкриття CF)
- Може переключити потік (якщо inner CF async)
thenApplyAsync / thenComposeAsync:
- + ~1μs на переключення потоку
Production Experience
Microservice call chain:
@Service
public class OrderFacade {
public CompletableFuture<OrderSummary> getOrderSummary(Long orderId) {
return orderRepository.findByIdAsync(orderId)
.thenCompose(order ->
userService.findByIdAsync(order.userId())
.thenApply(user -> new OrderSummary(order, user))
);
}
// vs з thenApply — якщо користувач вже в пам'яті
public OrderSummary getOrderSummarySync(Order order, User user) {
return CompletableFuture.completedFuture(order)
.thenApply(o -> new OrderSummary(o, user));
}
}
Best Practices
// ✅ thenApply для синхронних трансформацій
cf.thenApply(s -> s.toUpperCase());
// ✅ thenCompose для асинхронних викликів
cf.thenCompose(id -> fetchDataAsync(id));
// ✅ Ланцюжок thenCompose для workflow
cf.thenCompose(a -> callB(a))
.thenCompose(b -> callC(b));
// ❌ Вкладені CompletableFuture
// ❌ thenCompose коли можна thenApply
// ❌ Ігнорування помилок у ланцюжку
Коли НЕ використовувати thenCompose
Коли друга операція НЕ залежить від результату першої і може виконуватися паралельно.
У цьому випадку використовуйте thenCombine для паралельного виконання.
// ❌ thenCompose — послідовно, unnecessarily slow
CompletableFuture<User> user = getUserAsync(userId);
user.thenCompose(u -> getOrdersAsync(userId)); // не залежить від user!
// ✅ thenCombine — паралельно
CompletableFuture<User> user = getUserAsync(userId);
CompletableFuture<List<Order>> orders = getOrdersAsync(userId);
user.thenCombine(orders, UserProfile::new);
🎯 Шпаргалка для співбесіди
Обов’язково знати:
- thenApply — функція T -> U, для синхронних трансформацій (як map)
- thenCompose — функція T -> CompletableFuture, для асинхронних викликів (як flatMap)
- thenApply може створити вкладений CompletableFuture<CompletableFuture> — це баг
- thenCompose розкриває вкладеність, повертаючи CompletableFuture
- thenCompose виконує послідовно, thenCombine — паралельно
Часті уточнюючі питання:
- Коли thenApply, а коли thenCompose? — thenApply якщо функція повертає звичайне значення, thenCompose якщо повертає CompletableFuture
- Що буде якщо використати thenApply з async функцією? — Вийде CompletableFuture<CompletableFuture> — вкладеність, потрібно thenCompose
- thenCompose vs thenCombine — коли що? — thenCompose коли другий CF залежить від першого, thenCombine коли незалежні
- Який overhead у thenCompose? — ~10 ns на розкриття CF, плюс можливе переключення потоку
Червоні прапорці (НЕ говорити):
- «thenApply і thenCompose взаємозамінні» — thenApply створить вкладений CF
- «thenCompose швидший thenCombine» — thenCompose послідовний, thenCombine паралельний
- «thenApply завжди в тому ж потоці» — thenApplyAsync перемикає потік
Пов’язані теми:
- [[11. У чому різниця між thenApply() і thenApplyAsync()]]
- [[22. У чому різниця між thenCombine() і thenCompose()]]
- [[8. Як комбінувати результати декількох CompletableFuture]]
- [[1. Що таке CompletableFuture і чим він відрізняється від Future]]