1. Комбінуємо CompletableFuture
У реальному житті рідко все надходить з одного джерела: профіль користувача та його замовлення можна завантажувати паралельно, дані з двох мікросервісів — об’єднувати, а іноді хочеться обробити просто першу отриману відповідь. Синхронний підхід змушує чекати по черзі; клас CompletableFuture дозволяє запускати все одночасно й елегантно комбінувати результати. Для цього він надає спеціальні методи: thenCombine, allOf, anyOf. Розберімося з кожним по порядку.
Паралельні задачі: два асинхронні запити
Як це виглядало б синхронно:
String name = loadUserName(); // довго
int balance = loadUserBalance(); // довго
System.out.println("Ім’я: " + name + ", Баланс: " + balance);
Проблема: другий виклик почнеться лише після завершення першого.
Асинхронний спосіб
За допомогою CompletableFuture обидві задачі можна запустити одночасно:
CompletableFuture<String> nameFuture = CompletableFuture.supplyAsync(() -> loadUserName());
CompletableFuture<Integer> balanceFuture = CompletableFuture.supplyAsync(() -> loadUserBalance());
Але як тепер отримати обидва результати й обробити їх разом? Для цього слугує thenCombine.
2. thenCombine: об’єднуємо результати двох задач
Метод thenCombine дає змогу об’єднати два CompletableFuture і виконати дію, коли обидві задачі завершаться. Він повертає новий CompletableFuture з результатом об’єднання.
Сигнатура:
<A, B, C> CompletableFuture<C> thenCombine(
CompletionStage<? extends B> other,
BiFunction<? super A, ? super B, ? extends C> fn
)
- A — тип результату першого future,
- B — тип другого,
- C — тип об’єднаного результату.
Приклад
CompletableFuture<String> nameFuture = CompletableFuture.supplyAsync(() -> loadUserName());
CompletableFuture<Integer> balanceFuture = CompletableFuture.supplyAsync(() -> loadUserBalance());
CompletableFuture<String> resultFuture = nameFuture.thenCombine(
balanceFuture,
(name, balance) -> "Ім’я: " + name + ", Баланс: " + balance
);
resultFuture.thenAccept(System.out::println);
Як це працює:
- Обидва future запускаються паралельно.
- Щойно обидва завершилися, викликається функція (name, balance) -> ....
- Підсумковий future містить рядок із результатом.
Мініприклад із числами
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> 2);
CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> 3);
CompletableFuture<Integer> sum = f1.thenCombine(f2, Integer::sum);
sum.thenAccept(result -> System.out.println("Сума: " + result));
Виведення:
Сума: 5
Асинхронний варіант
Якщо об’єднання — важка операція, використовуйте thenCombineAsync:
f1.thenCombineAsync(f2, (a, b) -> a * b);
3. allOf: коли задач багато
А що, якщо в нас не дві задачі, а цілий десяток? Наприклад, ми хочемо паралельно завантажити дані одразу про десятьох користувачів. Для цього є метод CompletableFuture.allOf.
Опис
CompletableFuture.allOf(f1, f2, ..., fn) повертає новий future, який завершиться, коли завершаться всі передані задачі. Але є нюанс: цей future не містить результату — його тип завжди CompletableFuture<Void>. Щоб отримати результати, потрібно окремо «дістати» їх із вихідних future.
Приклад
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Перший");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "Другий");
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2);
all.thenRun(() -> {
// Усі задачі завершено!
String s1 = f1.join(); // join() — як get(), але кидає неперевірений виняток
String s2 = f2.join();
System.out.println(s1 + " & " + s2);
});
Виведення:
Перший & Другий
Приклад із масивом задач
List<CompletableFuture<String>> futures = new ArrayList<>();
for (int i = 0; i < 5; i++) {
int id = i;
futures.add(CompletableFuture.supplyAsync(() -> "Користувач " + id));
}
CompletableFuture<Void> all = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
all.thenRun(() -> {
for (CompletableFuture<String> f : futures) {
System.out.println(f.join());
}
});
Що відбувається:
- Усі задачі запускаються паралельно.
- allOf завершиться, коли завершаться всі задачі.
- У блоці thenRun ми можемо отримати результати через join().
Візуальна схема
[Future1] \
[Future2] ----> [allOf] ---> thenRun
[Future3] /
4. anyOf: чекаємо першу завершену задачу
Іноді потрібно не чекати на всіх, а отримати результат найшвидшої задачі. Наприклад, ми запитуємо дані з двох серверів — який відповість першим, того дані й використовуємо. Для цього є CompletableFuture.anyOf.
Опис
CompletableFuture.anyOf(f1, f2, ..., fn) повертає future, який завершиться, коли завершиться будь-яка з переданих задач. Тип результату — CompletableFuture<Object>, тому що типи задач можуть бути різними.
Приклад
CompletableFuture<String> fast = CompletableFuture.supplyAsync(() -> {
sleep(500);
return "Швидкий сервер";
});
CompletableFuture<String> slow = CompletableFuture.supplyAsync(() -> {
sleep(2000);
return "Повільний сервер";
});
CompletableFuture<Object> any = CompletableFuture.anyOf(fast, slow);
any.thenAccept(result -> System.out.println("Отримали: " + result));
Виведення:
Отримали: Швидкий сервер
Приклад із різними типами
Можна комбінувати задачі різних типів, але тоді результат буде типу Object, і знадобиться явне приведення типу.
5. Корисні нюанси
Рекомендації
- allOf не повертає масив результатів. Потрібно зберігати вихідні future, щоб отримати їхні значення через join() або get().
- anyOf повертає перший завершений результат, але тип — Object. Якщо всі задачі одного типу, можна здійснити приведення типу.
- Якщо одна з задач у allOf завершилася з помилкою — підсумковий future також завершиться з помилкою.
- Для thenCombine обидві задачі мають завершитися успішно, інакше — виняток.
Таблиця порівняння методів
| Метод | Коли використовувати | Тип результату |
|---|---|---|
|
Потрібно об’єднати результати двох задач | Результат об’єднання |
|
Потрібно дочекатися завершення всіх задач | |
|
Потрібно дочекатися завершення будь-якої з задач | Object (результат першої задачі) |
6. Типові помилки під час комбінування CompletableFuture
Помилка № 1: Очікування результату через get()/join() в головному потоці.
Якщо ви пишете асинхронний код, але в кінці все одно викликаєте get() або join(), то ви блокуєте потік і втрачаєте всі переваги асинхронності. Краще використовувати thenAccept/thenRun для обробки результату без блокування.
Помилка № 2: Не зберігаєте посилання на вихідні future під час використання allOf.
Якщо ви викликали CompletableFuture.allOf(f1, f2, f3), але не зберегли f1, f2, f3 — ви не зможете отримати їхні результати. allOf повертає лише Void!
Помилка № 3: Не обробляєте помилки в ланцюжку.
Якщо одна з задач завершиться з помилкою, увесь allOf або thenCombine також завершиться з помилкою. Використовуйте методи обробки помилок (exceptionally, handle, whenComplete), щоб не проґавити винятки.
Помилка № 4: Невідповідність типів у anyOf.
anyOf повертає Object. Якщо ваші future повертають різні типи, доведеться з’ясовувати, що саме прийшло першим. Краще використовувати однакові типи задач, якщо це можливо.
Помилка № 5: Надто складні ланцюжки без коментарів.
Коли коду стає багато, ланцюжки future можуть перетворитися на «локшину». Не соромтеся розбивати ланцюжок на окремі змінні та коментувати кроки.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ