JavaRush /Курси /JAVA 25 SELF /Комбінування CompletableFuture: thenCombine, allOf, anyOf...

Комбінування CompletableFuture: thenCombine, allOf, anyOf

JAVA 25 SELF
Рівень 55 , Лекція 2
Відкрита

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 обидві задачі мають завершитися успішно, інакше — виняток.

Таблиця порівняння методів

Метод Коли використовувати Тип результату
thenCombine
Потрібно об’єднати результати двох задач Результат об’єднання
allOf
Потрібно дочекатися завершення всіх задач
Void
anyOf
Потрібно дочекатися завершення будь-якої з задач 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 можуть перетворитися на «локшину». Не соромтеся розбивати ланцюжок на окремі змінні та коментувати кроки.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ