Gymterview
middle

Какие основные методы есть у CompletableFuture?

Методы CompletableFuture можно разделить на несколько категорий:

1. Создание:

  • supplyAsync(Supplier<U>) — запускает асинхронное вычисление, возвращающее результат.
  • runAsync(Runnable) — запускает асинхронное вычисление без возврата результата.
  • completedFuture(U value) — создаёт уже завершённый CompletableFuture с заданным значением.

2. Трансформация результата (map-подобные):

  • thenApply(Function<T, U>) — принимает результат предыдущего этапа, применяет функцию и возвращает новый CompletableFuture<U>. Аналог map в Stream.
  • thenApplyAsync(Function<T, U>) — то же, но выполняется в отдельном потоке.
Пример
CompletableFuture<Integer> lengthFuture = CompletableFuture
    .supplyAsync(() -> "Привет")
    .thenApply(String::length); // 6

3. Цепочка асинхронных операций (flatMap-подобные):

  • thenCompose(Function<T, CompletionStage<U>>) — принимает результат и возвращает новый CompletionStage. Аналог flatMap. Используется, когда каждый шаг сам является асинхронным.
Пример
CompletableFuture<String> result = CompletableFuture
    .supplyAsync(() -> getUserId())
    .thenCompose(userId -> CompletableFuture.supplyAsync(() -> fetchUser(userId)));

4. Потребление результата:

  • thenAccept(Consumer<T>) — потребляет результат, не возвращая значения.
  • thenRun(Runnable) — выполняет действие после завершения, не имея доступа к результату.

5. Комбинирование двух CompletableFuture:

  • thenCombine(CompletionStage<U>, BiFunction<T, U, V>) — ожидает завершения обоих и комбинирует результаты.
  • thenAcceptBoth(CompletionStage<U>, BiConsumer<T, U>) — потребляет оба результата.
  • runAfterBoth(CompletionStage<?>, Runnable) — выполняет действие после завершения обоих.
Пример
CompletableFuture<Double> priceFuture = CompletableFuture.supplyAsync(() -> getPrice());
CompletableFuture<Double> rateFuture = CompletableFuture.supplyAsync(() -> getExchangeRate());

CompletableFuture<Double> convertedPrice = priceFuture
    .thenCombine(rateFuture, (price, rate) -> price * rate);

6. Выбор первого завершённого:

  • applyToEither(CompletionStage<T>, Function<T, U>) — применяет функцию к результату первого завершившегося.
  • acceptEither(CompletionStage<T>, Consumer<T>) — потребляет результат первого завершившегося.
  • runAfterEither(CompletionStage<?>, Runnable) — выполняет действие после завершения любого из двух.

7. Обработка ошибок:

  • exceptionally(Function<Throwable, T>) — обрабатывает исключение, возвращая значение по умолчанию.
  • handle(BiFunction<T, Throwable, U>) — обрабатывает и результат, и исключение. Вызывается всегда.
  • whenComplete(BiConsumer<T, Throwable>) — выполняет действие при завершении, не изменяя результат.
Пример
CompletableFuture<String> safe = CompletableFuture
    .supplyAsync(() -> riskyOperation())
    .handle((result, ex) -> {
        if (ex != null) {
            log.error("Ошибка", ex);
            return "fallback";
        }
        return result;
    });

8. Множественные CompletableFuture:

  • allOf(CompletableFuture<?>...) — возвращает CompletableFuture<Void>, завершающийся, когда все переданные завершены.
  • anyOf(CompletableFuture<?>...) — возвращает CompletableFuture<Object>, завершающийся при завершении любого.
Код: сбор результатов через allOf
List<CompletableFuture<String>> futures = urls.stream()
    .map(url -> CompletableFuture.supplyAsync(() -> fetch(url)))
    .collect(Collectors.toList());

CompletableFuture<Void> allDone = CompletableFuture.allOf(
    futures.toArray(new CompletableFuture[0])
);

// Собрать все результаты после завершения
List<String> results = allDone.thenApply(v ->
    futures.stream()
        .map(CompletableFuture::join)
        .collect(Collectors.toList())
).join();

Ключевые различия:

Метод Аналог в Stream Назначение
thenApply map Синхронная трансформация
thenCompose flatMap Асинхронная трансформация (разворачивает вложенный CF)
thenCombine Объединение двух результатов
allOf Ожидание всех
anyOf Ожидание любого

Каждый метод существует в трёх вариантах:

  • thenApply(fn) — выполняется в потоке, завершившем предыдущий шаг.
  • thenApplyAsync(fn) — выполняется в ForkJoinPool.commonPool().
  • thenApplyAsync(fn, executor) — выполняется в указанном пуле.

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

  • Путаница между thenApply и thenCompose — приводит к CompletableFuture<CompletableFuture<T>>.
  • allOf возвращает Void — результаты нужно собирать отдельно из каждого CompletableFuture.
  • exceptionally после handlehandle уже обрабатывает исключение, exceptionally получит null.

На собеседовании ключевой вопрос: «Чем отличается thenApply от thenCompose?» Запомните: thenApply — это map, thenCompose — это flatMap. Если ваша функция сама возвращает CompletableFuture, используйте thenCompose, иначе получите вложенный future.