JavaRush /Курсы /JAVA 25 SELF /Комбинирование CompletableFuture: thenCombine, allOf, any...

Комбинирование 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(), но бросает unchecked exception
    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 могут превратиться в «лапшу». Не стесняйтесь разбивать цепочку на отдельные переменные и комментировать шаги.

1
Задача
JAVA 25 SELF, 55 уровень, 2 лекция
Недоступна
Координация Поставок: Ожидание Всех Грузов
Координация Поставок: Ожидание Всех Грузов
1
Задача
JAVA 25 SELF, 55 уровень, 2 лекция
Недоступна
Сборка Приветствия: Объединение Фрагментов Речи
Сборка Приветствия: Объединение Фрагментов Речи
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ