В чём разница между thenCombine() и thenCompose()
Оба метода связывают два вычисления CompletableFuture, но они служат совершенно разным целям:
🟢 Junior Level
Оба метода связывают два вычисления CompletableFuture, но они служат совершенно разным целям:
thenCombine()используется для параллельного объединения двух независимых задач. Задачи выполняются одновременно, а их результаты объединяются третьей функцией (аналогzip).thenCompose()используется для последовательного связывания двух зависимых задач. Вторая задача стартует только тогда, когда завершилась первая, используя её результат как входной параметр (аналогflatMap).
Наглядное сравнение
// 1. thenCombine: задачи НЕ ЗАВИСЯТ друг от друга (выполняются ПАРАЛЛЕЛЬНО)
CompletableFuture<Double> usdRateFuture = CompletableFuture.supplyAsync(() -> getUsdRate());
CompletableFuture<Double> eurRateFuture = CompletableFuture.supplyAsync(() -> getEurRate());
CompletableFuture<String> ratesReport = usdRateFuture.thenCombine(
eurRateFuture,
(usd, eur) -> "USD: " + usd + ", EUR: " + eur
);
// Задержка = max(время_usd, время_eur)
// 2. thenCompose: задача 2 НЕ МОЖЕТ начаться без задачи 1 (выполняются ПОСЛЕДОВАТЕЛЬНО)
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> fetchUser(userId));
CompletableFuture<List<Order>> ordersFuture = userFuture.thenCompose(
user -> fetchOrdersAsync(user.getAccountId()) // Нужен accountId из user!
);
// Задержка = время_user + время_orders
🟡 Middle Level
Сравнительная матрица
| Характеристика | thenCombine |
thenCompose |
|---|---|---|
| Функциональная абстракция | Аппликативный функтор (zipWith / liftA2) |
Монада (bind / flatMap) |
| Сигнатура аргумента | CompletionStage<U>, BiFunction<T, U, V> |
Function<T, CompletionStage<U>> |
| Зависимость данных | Задачи независимы | Задача 2 зависит от результата Задачи 1 |
| Порядок выполнения | Конкурентно / Параллельно | Строго последовательно |
| Итоговое время (Latency) | $\max(T_1, T_2) + T_{\text{combiner}}$ | $T_1 + T_2$ |
| Парные аналоги | thenAcceptBoth, runAfterBoth |
— |
Типичная ошибка: случайная сериализация параллельных вызовов
Частая ошибка разработчиков — использование thenCompose там, где вызовы независимы. Это превращает параллельную обработку в последовательную, удваивая время ответа:
// ❌ АНТИПАТТЕРН: погода и пробки не зависят друг от друга, но выполняются последовательно!
CompletableFuture<Dashboard> bad = fetchWeatherAsync()
.thenCompose(weather -> fetchTrafficAsync()
.thenApply(traffic -> new Dashboard(weather, traffic)));
// Итоговое время = 200 мс + 200 мс = 400 мс
// ✅ ПРАВИЛЬНО: запустить оба запроса одновременно
CompletableFuture<Weather> weatherF = fetchWeatherAsync();
CompletableFuture<Traffic> trafficF = fetchTrafficAsync();
CompletableFuture<Dashboard> good = weatherF.thenCombine(
trafficF,
(weather, traffic) -> new Dashboard(weather, traffic)
);
// Итоговое время = max(200 мс, 200 мс) = 200 мс!
🔴 Senior Level
Поведение при ошибках: изоляция задач
Фундаментальное архитектурное отличие кроется в поведении при сбоях:
- В
thenCompose(Fail-Early):- Если первый фьючерс падает с исключением, функция создания второго фьючера никогда не вызывается.
- Второй сетевой запрос даже не будет отправлен.
- В
thenCombine(Background Leaks):- Оба фьючера
cf1иcf2запускаются параллельно в фоновых пулах. - Если
cf1падает через 10 мс, результирующий фьючерсthenCombineнемедленно завершается ошибкойCompletionException. - Но
cf2продолжает работать в фоне до конца! Никакой автоматической отменыcf2рантаймthenCombineне производит.
- Оба фьючера
Проблема сохранения промежуточного контекста в thenCompose
Когда в конвейере из 3 шагов последнему шагу нужны результаты всех предыдущих:
// Решение через запись в record-кортеж (без вложенного callback hell):
fetchUserAsync(id)
.thenCompose(user -> fetchAccountAsync(user).thenApply(acc -> new UserAccount(user, acc)))
.thenCompose(ua -> fetchCardsAsync(ua.acc()).thenApply(cards -> new Profile(ua.user(), cards)));
4 Tricky Questions
1. Каким категориям функционального программирования соответствуют методы thenCompose и thenCombine?
Ответ:
thenComposeреализует интерфейс Монады (Monad): операциюbind(илиflatMap). Он принимает функцию $T \to M[U]$ и разворачивает вложенный контекст $M[M[U]] \to M[U]$.thenCombineреализует интерфейс Аппликативного функтора (Applicative Functor): операциюliftA2/zipWith. Он берёт два независимых контекста $M[T]$ и $M[U]$ и объединяет их с помощью чистой би-функции $(T, U) \to V$ в $M[V]$.
2. Если в cf1.thenCombine(cf2, (a, b) -> a + b) первый фьючерс cf1 падает с исключением, что произойдёт с выполнением cf2?
Ответ:
Фьючерс cf2 продолжит своё выполнение в фоне до конца.
Результирующий фьючерс немедленно завершится с ошибкой, но thenCombine не отменяет второй параллельный фьючерс автоматически. Если cf2 выполнял дорогую операцию записи в БД или отправки письма, она будет завершена, если вы вручную не отмените её через обработчики ошибок.
3. Можно ли выразить thenCombine через thenCompose? Каковы будут последствия?
Ответ: Да, синтаксически можно:
// Если cf1 и cf2 уже запущены параллельно:
cf1.thenCompose(r1 -> cf2.thenApply(r2 -> combine(r1, r2)));
Если cf2 был создан до вызова thenCompose, вычисления будут идти параллельно.
Но если фабрика второго фьючера вызвана внутри лямбды:
cf1.thenCompose(r1 -> fetchCf2().thenApply(r2 -> combine(r1, r2)));
Вычисление станет строго последовательным, и время ожидания возрастет с $\max(t_1, t_2)$ до $t_1 + t_2$.
4. В каком потоке выполнится функция-комбинатор (a, b) -> ... в методе thenCombine?
Ответ:
Функция-комбинатор выполнится в том потоке, который завершил последний (самый медленный) из двух фьючеров.
Если к моменту вызова thenCombine оба фьючера уже были завершены, функция выполнится синхронно в вызывающем потоке. Для явного переноса выполнения комбинатора в выделенный пул используется thenCombineAsync(cf2, fn, executor).
🎯 Шпаргалка для интервью
Главное за 60 секунд
thenCombine= параллельно, независимо, объединение двух готовых результатов (Applicative /zip). Latency = $\max(t_1, t_2)$.thenCompose= последовательно, зависимо, второй фьючерс стартует по результату первого (Monad /flatMap). Latency = $t_1 + t_2$.- При ошибке в
thenComposeвторая задача даже не стартует. - При ошибке в
thenCombineвторая задача продолжает работать в фоне (нет авто-отмены). - Перепутать
thenCombineиthenComposeдля независимых задач означает удвоить задержку (Latency) сервиса.
Типичные ошибки на интервью
- Использовать
thenComposeдля независимых запросов, разрушая параллелизм. - Считать, что
thenCombineотменяет второй фьючерс при сбое первого. - Путать входные сигнатуры:
thenCombineпринимаетBiFunction,thenComposeпринимаетFunction, возвращающуюCompletionStage.