У чому різниця між 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.