Gymterview
middle

Что такое CompletableFuture и как он работает?

CompletableFuture — это класс из java.util.concurrent (Java 8), реализующий интерфейсы Future и CompletionStage. Он представляет результат асинхронного вычисления, который можно явно завершить, и предоставляет богатый API для построения цепочек асинхронных операций.

Отличие от обычного Future: Future (Java 5) позволяет лишь проверить завершённость и получить результат блокирующим вызовом get(). CompletableFuture добавляет:

  • Неблокирующие цепочки операций (thenApply, thenCompose)
  • Комбинирование нескольких задач (thenCombine, allOf, anyOf)
  • Обработку ошибок в функциональном стиле (exceptionally, handle)
  • Возможность ручного завершения (complete(), completeExceptionally())

Принцип работы — «обещание» (promise):

Пример
// Создание и ручное завершение
CompletableFuture<String> future = new CompletableFuture<>();
// В другом потоке:
future.complete("Результат");
// Или при ошибке:
future.completeExceptionally(new RuntimeException("Ошибка"));

// Создание с автоматическим выполнением в ForkJoinPool.commonPool()
CompletableFuture<String> asyncFuture = CompletableFuture.supplyAsync(() -> {
    // длительная операция
    return "Результат асинхронного вычисления";
});

// С явным пулом потоков
ExecutorService executor = Executors.newFixedThreadPool(4);
CompletableFuture<String> customPoolFuture = CompletableFuture.supplyAsync(() -> {
    return "Результат в custom pool";
}, executor);

Построение цепочек:

Пример
CompletableFuture<String> result = CompletableFuture
    .supplyAsync(() -> fetchDataFromDB())       // Асинхронно получить данные
    .thenApply(data -> transform(data))          // Преобразовать результат
    .thenApply(transformed -> format(transformed)) // Отформатировать
    .exceptionally(ex -> "Значение по умолчанию"); // Обработать ошибку

Комбинирование нескольких CompletableFuture:

Пример
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Привет");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> " Мир");

// Комбинирование двух результатов
CompletableFuture<String> combined = future1.thenCombine(future2, (s1, s2) -> s1 + s2);
System.out.println(combined.get()); // "Привет Мир"

// Ожидание завершения всех
CompletableFuture<Void> all = CompletableFuture.allOf(future1, future2);

// Ожидание первого завершённого
CompletableFuture<Object> any = CompletableFuture.anyOf(future1, future2);

Ключевые нюансы:

  • По умолчанию используется ForkJoinPool.commonPool(). Для IO-задач рекомендуется передавать собственный ExecutorService.
  • Методы с суффиксом Async (например, thenApplyAsync) выполняют следующий шаг в отдельном потоке, без суффикса — в потоке, завершившем предыдущий шаг.
  • CompletableFuture можно завершить вручную через complete(), что удобно для интеграции с callback-based API.
  • Цепочки операций формируют направленный ациклический граф (DAG) зависимостей.

Частые ошибки:

  • get() без таймаута — может заблокировать поток навсегда. Используйте get(timeout, unit) или join().
  • Игнорирование исключений — без exceptionally() или handle() исключение обнаружится только при get().
  • Блокирующие IO в ForkJoinPool.commonPool() — это общий пул, блокировка его потоков приводит к голоданию.

Актуальность в 2026: CompletableFuture остаётся основным инструментом для оркестрации асинхронных задач. С Virtual Threads (Java 21+) необходимость в нём для простых IO-задач снижается, но для сложных сценариев комбинирования он по-прежнему незаменим.

Аналогия: CompletableFuture — это заказ в ресторане. Вы оформляете заказ (supplyAsync) и получаете номерок (объект future). Пока заказ готовится, вы можете добавить просьбу «когда будет готово — подогрейте» (thenApply), «если не получится — принесите альтернативу» (exceptionally). Когда блюдо готово, вся цепочка инструкций выполняется автоматически.

На собеседовании часто спрашивают: «В каком потоке выполняется thenApply?» Ответ: в потоке, завершившем предыдущий этап, или в вызывающем потоке, если future уже завершён. Для гарантированного выполнения в пуле используйте thenApplyAsync.