Что такое CompletableFuture в Java и для чего он используется?

Ответ

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

Ключевые возможности:

  1. Явное завершение: Вы можете завершить CompletableFuture вручную с результатом (complete(T value)) или исключением (completeExceptionally(Throwable ex)).
  2. Цепочки вызовов (chaining): Позволяет описывать поток обработки: "когда завершится эта задача, сделай то, затем другое".
  3. Комбинация нескольких Future: Объединение результатов двух и более независимых асинхронных задач.
  4. Гибкая обработка исключений прямо в цепочке.
  5. Асинхронное выполнение с использованием пулов потоков (ForkJoinPool.commonPool() по умолчанию или своего).

Основные методы для построения цепочек:

  • thenApply(Function) — преобразует результат (синхронно).
  • thenApplyAsync(Function) — преобразует результат асинхронно.
  • thenAccept(Consumer) — потребляет результат без возврата значения.
  • thenCompose(Function) — "плоское" преобразование (композиция Future).
  • thenCombine(CompletionStage, BiFunction) — объединяет результаты двух Future.
  • exceptionally(Function) — обрабатывает исключение, возвращая резервное значение.
  • handle(BiFunction) — обрабатывает и результат, и исключение.

Практический пример: параллельный вызов двух сервисов и объединение результата.

import java.util.concurrent.CompletableFuture;

public class OrderService {
    // Имитация асинхронных вызовов
    CompletableFuture<Double> getPriceAsync(String productId) {
        return CompletableFuture.supplyAsync(() -> {
            // Долгий вызов к Price Service
            return 99.99;
        });
    }

    CompletableFuture<Double> getDiscountAsync(String userId) {
        return CompletableFuture.supplyAsync(() -> {
            // Долгий вызов к Discount Service
            return 0.1; // 10% скидка
        });
    }

    public CompletableFuture<Double> calculateTotalAsync(String productId, String userId) {
        CompletableFuture<Double> priceFuture = getPriceAsync(productId);
        CompletableFuture<Double> discountFuture = getDiscountAsync(userId);

        // Комбинируем результаты, когда оба Future завершатся
        return priceFuture.thenCombine(discountFuture, (price, discountRate) -> {
            return price * (1 - discountRate); // Итоговая цена со скидкой
        }).exceptionally(ex -> { // Обработка ошибок в любой из задач
            System.err.println("Ошибка расчета: " + ex.getMessage());
            return 0.0; // Возвращаем значение по умолчанию
        });
    }

    public static void main(String[] args) throws Exception {
        OrderService service = new OrderService();
        CompletableFuture<Double> totalFuture = service.calculateTotalAsync("prod1", "user42");

        // Блокирующее ожидание результата (в реальном коде лучше использовать thenAccept)
        Double total = totalFuture.get();
        System.out.println("Итоговая цена: " + total); // Выведет: Итоговая цена: 89.991
    }
}

Использование: CompletableFuture — основной инструмент для неблокирующего асинхронного программирования в современных Java-приложениях, особенно в микросервисных архитектурах и высоконагруженных веб-сервисах.

Ответ 18+ 🔞

А, блядь, CompletableFuture! Ну это ж, сука, просто песня, а не класс! Появился в Java 8, как гром среди ясного неба, и сразу всем показал, где раки зимуют.

Представь себе, ты раньше с этим Future обычным мучился — как об стенку горох, нихуя не понятно, когда там твой результат подъедет. А тут — овердохуища возможностей! Это как если бы тебе дали не просто обещание, что пиццу привезут, а ещё и возможность сказать: «Когда привезут, разогрей её, потом посыпь перчиком, а потом позови меня, когда будет готова». И всё это — не стоя над курьером с палкой!

Что он умеет, этот хитрожопый ублюдок:

  1. Сам решать, когда всё готово. Можешь вручную ткнуть ему в морду результат (complete()) или крикнуть: «Всё, пиздец, ошибка!» (completeExceptionally()). Сам хозяин положения, блядь.
  2. Строить цепочки, как паровозик. «Сделай это — потом то — а потом вот эдак». Это называется CompletionStage, и это просто волшебство, ёпта.
  3. Скрещивать несколько Future, как бог черепаху с уткой. Ждёт, пока два независимых долбоёба-сервиса ответят, а потом из их ответов лепит одну общую сосиску.
  4. Ловить исключения прямо в цепочке, не разваливая всё к хуям собачьим. Красота!
  5. Гонять задачи в пулах потоков асинхронно, чтобы твой главный поток не стоял столбом, как идиот.

Основные рычаги и кнопки, которыми дёргаешь:

  • thenApply() — взял результат, обработал и отдал дальше. Синхронно, не отходя от кассы.
  • thenApplyAsync() — то же самое, но пинком под жопу отправляет в другой поток, чтобы не засирать текущий.
  • thenAccept() — взял результат, что-то с ним сделал (например, вывел на экран) и пошёл дальше. Ничего не возвращает, как благородный донор.
  • thenCompose() — это, блядь, высший пилотаж. Берешь один CompletableFuture, а возвращаешь другой. Плоское преобразование, ёбта! Чтобы не было CompletableFuture<CompletableFuture<T>> — этой хуйни, от которой мозг вскипает.
  • thenCombine() — ждёт два будущих результата, а потом скрещивает их в одном методе. Любовь, блядь, на атомном уровне.
  • exceptionally() — спасательный круг. Если где-то в цепочке всё пошло по пизде, ты здесь можешь подстелить соломки и вернуть какое-нибудь дефолтное значение.
  • handle() — универсальный солдат. Обрабатывает и удачный результат, и пиздец (исключение). Всё в одном флаконе.

Смотри, как это выглядит в жизни, на примере, от которого волосы дыбом встают:

Допустим, тебе надо посчитать цену заказа. Но цена лежит в одном сервисе (который тормозит, как черепаха в сиропе), а скидка — в другом (который отвечает, когда ему вздумается). Раньше бы ты их последовательно вызывал и ждал, как лох. А теперь — хуяк-хуяк, и параллельно!

import java.util.concurrent.CompletableFuture;

public class OrderService {
    // Прикинемся, что это долгий вызов к сервису цен
    CompletableFuture<Double> getPriceAsync(String productId) {
        return CompletableFuture.supplyAsync(() -> {
            // Симуляция долгой работы, блядь
            try { Thread.sleep(1000); } catch (InterruptedException e) {}
            return 99.99; // Цена
        });
    }

    // А это — вызов к сервису скидок, который тоже не шибко шустрый
    CompletableFuture<Double> getDiscountAsync(String userId) {
        return CompletableFuture.supplyAsync(() -> {
            try { Thread.sleep(800); } catch (InterruptedException e) {}
            return 0.1; // 10% скидки, мать его
        });
    }

    public CompletableFuture<Double> calculateTotalAsync(String productId, String userId) {
        CompletableFuture<Double> priceFuture = getPriceAsync(productId);
        CompletableFuture<Double> discountFuture = getDiscountAsync(userId);

        // А вот и магия! Ждём ОБА результата, а потом хуячим формулу
        return priceFuture.thenCombine(discountFuture, (price, discountRate) -> {
            return price * (1 - discountRate); // Итоговая цена
        }).exceptionally(ex -> { // Если где-то случилась жопа
            System.err.println("Ошибка расчета, ёпта: " + ex.getMessage());
            return 0.0; // Отдаём ноль, чтоб не палить систему
        });
    }

    public static void main(String[] args) throws Exception {
        OrderService service = new OrderService();
        CompletableFuture<Double> totalFuture = service.calculateTotalAsync("prod1", "user42");

        // Тут мы ждём результат (в реальности так лучше не делать, а подписываться через thenAccept)
        Double total = totalFuture.get(); // get() — это блокирующий вызов, старый дед
        System.out.println("Итоговая цена: " + total); // Выведет что-то вроде: Итоговая цена: 89.991
    }
}

И где этот красавец применяется? Да везде, сука! Особенно в этих ваших модных микросервисах, где один запрос должен пообщаться с десятком других сервисов. Вместо того чтобы тупо ждать ответа от каждого по очереди (и тратить время, как последний лох), ты запускаешь все запросы параллельно, а потом, как главный дирижёр, собираешь из них общий ответ. Производительность взлетает до небес, а код выглядит почти что поэзией. Ну, или как минимум — грамотно написанной прозой, а не потоком сознания дегенерата.