Що робить метод supplyAsync() і коли його використовувати
Метод CompletableFuture.supplyAsync() — це головний фабричний метод для запуску асинхронних обчислень, що повертають результат. Він приймає функціональний інтерфейс Supplier
🟢 Junior Level
Метод CompletableFuture.supplyAsync() — це головний фабричний метод для запуску асинхронних обчислень, що повертають результат. Він приймає функціональний інтерфейс Supplier<T> (постачальник даних) та ініціює виконання завдання у фоновому пулі потоків.
Базовий приклад
// Запуск асинхронного завдання:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// Код виконується у фоновому потоці
return "Дані із зовнішнього сервісу";
});
// Неблокуюче споживання результату:
future.thenAccept(result -> System.out.println("Отримано: " + result));
Відмінність supplyAsync() від runAsync()
| Метод | Інтерфейс, що приймається | Повернене значення | Типове застосування |
|---|---|---|---|
supplyAsync |
Supplier<T> (() -> T) |
CompletableFuture<T> |
Завантаження даних, обчислення, читання |
runAsync |
Runnable (() -> void) |
CompletableFuture<Void> |
Відправка сповіщень, аудит, “fire-and-forget” |
// supplyAsync — повертає значення T
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> fetchUser(42));
// runAsync — побічний ефект без результату
CompletableFuture<Void> auditFuture = CompletableFuture.runAsync(() -> sendAuditLog("User 42 logged in"));
🟡 Middle Level
Перевантаження методу та ізоляція пулів
Метод існує у двох варіантах:
supplyAsync(Supplier<U> supplier): відправляє завдання в дефолтний системний пулForkJoinPool.commonPool().supplyAsync(Supplier<U> supplier, Executor executor): відправляє завдання у вказаний користувацькийExecutor.
// Для I/O операцій ЗАВЖДИ передавайте виділений пул потоків:
ExecutorService ioExecutor = Executors.newFixedThreadPool(16);
CompletableFuture<List<Product>> productsFuture = CompletableFuture.supplyAsync(
() -> catalogClient.getProducts(),
ioExecutor
);
Проблема перевірюваних винятків (Checked Exceptions)
Функціональний інтерфейс Supplier<T> оголошує метод T get(), який не декларує перевірюваних винятків (throws Exception).
Якщо ваш метод викидає, наприклад, IOException чи SQLException, компілятор видасть помилку:
// ❌ ПОМИЛКА КОМПІЛЯЦІЇ: unhandled IOException
CompletableFuture.supplyAsync(() -> Files.readString(Path.of("config.json")));
// ✅ Рішення 1: традиційний try-catch із загортанням у RuntimeException
CompletableFuture.supplyAsync(() -> {
try {
return Files.readString(Path.of("config.json"));
} catch (IOException e) {
throw new UncheckedIOException(e); // Обернеться в CompletionException
}
});
// ✅ Рішення 2: SneakyThrows (Lombok) або універсальний ThrowingSupplier
CompletableFuture.supplyAsync(rethrow(() -> Files.readString(Path.of("config.json"))));
🔴 Senior Level
Внутрішня будова: завдання AsyncSupply
Під капотом виклик CompletableFuture.supplyAsync(supplier, executor) влаштований наступним чином:
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor) {
return asyncSupplyStage(screenExecutor(executor), supplier);
}
static <U> CompletableFuture<U> asyncSupplyStage(Executor e, Supplier<U> f) {
if (f == null) throw new NullPointerException();
CompletableFuture<U> d = new CompletableFuture<U>();
e.execute(new AsyncSupply<U>(d, f)); // Загортаємо у внутрішнє завдання
return d;
}
Клас AsyncSupply успадковує ForkJoinTask<Void> і реалізує Runnable та AsynchronousCompletionTask:
static final class AsyncSupply<T> extends ForkJoinTask<Void> implements Runnable, AsynchronousCompletionTask {
CompletableFuture<T> dep;
Supplier<? extends T> fn;
AsyncSupply(CompletableFuture<T> dep, Supplier<? extends T> fn) {
this.dep = dep; this.fn = fn;
}
public void run() {
CompletableFuture<T> d; Supplier<? extends T> f;
if ((d = dep) != null && (f = fn) != null) {
dep = null; fn = null; // Зануляємо посилання для збирача сміття!
if (d.result == null) {
try {
d.completeValue(f.get()); // Успішний запис через CAS
} catch (Throwable ex) {
d.completeThrowable(ex); // Запис AltResult(ex)
}
}
d.postComplete(); // Пробудження всіх підписників у стеку
}
}
}
Ключові архітектурні висновки:
- Посилання на
depтаfnзануляються одразу після старту виконання, запобігаючи витокам пам’яті (Memory Leaks), якщо лямбда захопила великий контекст. - Будь-який
Throwable(включаючиErrorтаRuntimeException) перехоплюється і надійно фіксується в поліresult, переводячи ф’ючер у стан винятку без краху потоку пулу.
4 Tricky Questions
1. Що станеться, якщо supplier.get() всередині supplyAsync поверне null? Чи вважається це помилкою?
Відповідь:
Ні, це не вважається помилкою.
Метод завершиться успішно. Всередині CompletableFuture значення null розпізнається та упаковується в спеціальний синглтон NIL (екземпляр AltResult із порожнім полем винятку). При подальшому виклику future.join() або передачі аргументу в thenAccept(val -> ...) значення NIL прозоро розпакується назад у звичайний null.
2. У чому різниця між executorService.submit(callable) та CompletableFuture.supplyAsync(supplier, executorService)?
Відповідь:
executorService.submit(Callable<T>)повертає класичний блокуючийFuture<T>(Java 5). У нього немає методів композиції, комбінування та декларативної обробки помилок без блокуванняget(). Крім того,Callableможе оголошувати перевірювані винятки (throws Exception).CompletableFuture.supplyAsync(Supplier<T>, executor)повертаєCompletableFuture<T>з підтримкою контрактуCompletionStage(40+ методів неблокуючої композиції). ПротеSupplierне дозволяє кидати checked-винятки без обгортки.
3. Якщо передане в supplyAsync() завдання зависло, і ми викликали future.cancel(true), чи перерветься виконання завдання в потоці?
Відповідь:
Ні, завдання продовжить виконуватися до кінця!
На відміну від FutureTask, у CompletableFuture.cancel(boolean mayInterruptIfRunning) прапорець mayInterruptIfRunning повністю ігнорується. Метод cancel() лише атомарно записує CancellationException у сам об’єкт CompletableFuture. Сигнал Thread.interrupt() на виконуючий потік воркера не надсилається. Якщо потрібна переривність, необхідно передавати зовнішні прапорці або використовувати класичний FutureTask.
4. Що станеться, якщо передати null як supplier: CompletableFuture.supplyAsync(null)?
Відповідь:
Метод негайно викине NullPointerException у викликаючому потоці ще до постановки завдання в чергу екзекутора.
Рантайм CompletableFuture виконує попередню сувору валідацію вхідних аргументів (if (f == null) throw new NullPointerException();), захищаючи конвеєр від створення невалідних порожніх завдань.
🎯 Шпаргалка для інтерв’ю
Головне за 60 секунд
supplyAsync(Supplier<T>)— асинхронний запуск завдання з поверненням значенняT.runAsync(Runnable)— асинхронний запуск завдання без повернення значення (Void).- Без другого аргументу використовується
ForkJoinPool.commonPool(). Для будь-яких I/O операцій обов’язково передавати свійExecutor. Supplierне підтримує Checked Exceptions — їх потрібно загортати вRuntimeException.- Повернення
nullз лямбди абсолютно легальне і завершує ф’ючер успіхом. future.cancel(true)не перериває потік виконання завданняsupplyAsync.
Типові помилки на інтерв’ю
- Плутати
supplyAsync(повертаєT) таrunAsync(повертаєVoid). - Вважати, що
cancel(true)надсилаєThread.interrupt()на потік, що виконуєsupplyAsync. - Запускати блокуючий код через
supplyAsyncбез вказівки ізольованого пулу.