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 обе задачи должны завершиться успешно, иначе — исключение.
Таблица сравнения методов
| Метод | Когда использовать | Тип результата |
|---|---|---|
|
Нужно объединить результаты двух задач | Результат объединения |
|
Нужно дождаться завершения всех задач | |
|
Нужно дождаться завершения любой из задач | 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 могут превратиться в «лапшу». Не стесняйтесь разбивать цепочку на отдельные переменные и комментировать шаги.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ