CompletableFuture

CompletableFuture — способ запустить задачу асинхронно и продолжить работу без блокировки, а когда результат будет готов — обработать его. В отличие от Future (который можно только ждать через .get()), CompletableFuture позволяет задать цепочку обработки.

// Запустить задачу в фоне
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    return fetchDataFromApi(); // выполнится в другом потоке
});

// Когда результат готов — обработать (тоже асинхронно)
future
    .thenApply(data -> parseJson(data))      // преобразовать результат
    .thenAccept(user -> saveToDb(user))      // использовать (без возврата)
    .exceptionally(ex -> {                   // обработать ошибку
        log.error("Ошибка: " + ex.getMessage());
        return null;
    });

Основные методы:

// Запуск
CompletableFuture.runAsync(() -> doWork())          // без результата
CompletableFuture.supplyAsync(() -> "результат")    // с результатом

// Цепочка
.thenApply(r -> transform(r))     // преобразовать: T → U
.thenAccept(r -> consume(r))      // использовать: T → void
.thenRun(() -> doNext())          // выполнить следующее: () → void
.thenCompose(r -> anotherFuture(r)) // flatMap для CF

// Параллельно
CompletableFuture.allOf(cf1, cf2, cf3)  // подождать все
CompletableFuture.anyOf(cf1, cf2, cf3)  // подождать первый
Копнуть глубже

thenApply vs thenCompose. Как map vs flatMap в Stream:

  • thenApply(f)f возвращает значение → оборачивается в CF
  • thenCompose(f)f сама возвращает CF → не оборачивается (нет вложенности)
// thenApply: CF<CF<User>> — плохо, вложенный Future
CompletableFuture<CompletableFuture<User>> nested =
    cf.thenApply(id -> findUserAsync(id));

// thenCompose: CF<User> — хорошо, плоский результат
CompletableFuture<User> flat =
    cf.thenCompose(id -> findUserAsync(id));

Пул потоков по умолчанию. supplyAsync без второго аргумента использует ForkJoinPool.commonPool() — общий пул для всего приложения. Для I/O-задач (сеть, БД) лучше передавать свой executor, иначе можно заблокировать общий пул долгими задачами:

ExecutorService ioPool = Executors.newFixedThreadPool(20);
CompletableFuture.supplyAsync(() -> callDatabase(), ioPool);

Комбинирование результатов:

CompletableFuture<User>    userFuture    = fetchUserAsync(id);
CompletableFuture<Account> accountFuture = fetchAccountAsync(id);

// Подождать оба и объединить
CompletableFuture<String> result = userFuture.thenCombine(
    accountFuture,
    (user, account) -> user.name() + ": " + account.balance()
);

join() vs get(). Оба блокируют до получения результата. Разница: get() бросает checked InterruptedException + ExecutionException, join() — unchecked CompletionException. В цепочке CF обычно используют join().

🎤 Закрыл тему, если можешь объяснить:
• зачем CompletableFuture и чем он лучше обычного Future;
• разницу thenApply и thenCompose, как объединить два CF через thenCombine (если дошёл до 2-го слоя).