В чём разница между 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]]