JavaRush /Курси /JAVA 25 SELF /Асинхронні завдання: thenApply, thenAccept, thenRun

Асинхронні завдання: thenApply, thenAccept, thenRun

JAVA 25 SELF
Рівень 55 , Лекція 1
Відкрита

1. Запуск асинхронного завдання: supplyAsync і runAsync

Найпоширеніший спосіб запустити асинхронне завдання — використати CompletableFuture.supplyAsync. Цей метод приймає лямбду або метод, який повертає результат. Наприклад, ми хочемо імітувати завантаження даних із сервера:

import java.util.concurrent.CompletableFuture;

public class Main {
    public static void main(String[] args) {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            // Імітація довгої операції (наприклад, завантаження файлу)
            sleep(1000);
            return "Дані з сервера";
        });

        System.out.println("Завдання запущено!");
        // ... тут можна робити щось іще, поки завдання виконується
    }

    private static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
    }
}

runAsync: коли результат не потрібен

Якщо ваше завдання нічого не повертає (наприклад, просто пише в лог, надсилає сповіщення), використовуйте runAsync:

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    sleep(500);
    System.out.println("Операцію завершено!");
});

runAsync завжди повертає CompletableFuture<Void>, тому що результат не передбачається.

2. thenApply, thenAccept, thenRun: у чому різниця?

Коли асинхронне завдання завершилося, зазвичай хочеться щось зробити з результатом. Для цього придумані методи‑«обробники»:

  • thenApply — перетворює результат і повертає новий результат.
  • thenAccept — приймає результат, нічого не повертає (використовується для побічних ефектів).
  • thenRun — не приймає результат, нічого не повертає (просто виконує дію після завершення завдання).

thenApply: обробка і перетворення результату

Якщо вам потрібно перетворити результат попередньої задачі, використовуйте thenApply. Наприклад, завантажили рядок, а тепер хочемо дізнатися його довжину:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Java");

CompletableFuture<Integer> lengthFuture = future.thenApply(s -> {
    System.out.println("Обчислюємо довжину рядка...");
    return s.length();
});

// lengthFuture тепер містить Integer — довжину рядка "Java"
lengthFuture.thenAccept(len -> System.out.println("Довжина: " + len));

Що відбувається:

  • future містить рядок "Java".
  • thenApply перетворює рядок на його довжину (int).
  • thenAccept виводить результат.

thenAccept: дія з результатом (нічого не повертає)

Якщо потрібно просто щось зробити з результатом (наприклад, вивести його на екран), а повертати нічого не треба — використовуйте thenAccept:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Привіт, світе!");

future.thenAccept(result -> {
    System.out.println("Результат: " + result);
});

thenAccept — це як «споживач»: він «з’їдає» результат і робить із ним щось корисне.

thenRun: дія без результату

Якщо ви хочете просто виконати якусь дію після завершення завдання, але результат вам не потрібен, використовуйте thenRun:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Готово!");

future.thenRun(() -> {
    System.out.println("Завантаження завершено!");
});

Зверніть увагу: всередині thenRun ви не можете отримати результат попереднього завдання — він просто ігнорується.

3. Ланцюжки викликів: будуємо pipeline із завдань

Найбільша сила CompletableFuture — це вміння будувати ланцюжки обчислень. Кожен метод (thenApply, thenAccept, thenRun) повертає новий CompletableFuture, до якого можна знову додати обробник.

Приклад: багатоступенева обробка

Давайте доробимо наш застосунок: завантажимо дані, перетворимо їх, виведемо результат і напишемо в лог, що все завершено.

CompletableFuture.supplyAsync(() -> {
    System.out.println("Крок 1: Завантажуємо дані...");
    sleep(500);
    return "Java";
})
.thenApply(data -> {
    System.out.println("Крок 2: Перетворюємо дані...");
    return data.toUpperCase();
})
.thenAccept(result -> {
    System.out.println("Крок 3: Виводимо результат: " + result);
})
.thenRun(() -> {
    System.out.println("Крок 4: Усе завершено!");
});

Виведення у консоль:

Крок 1: Завантажуємо дані...
Крок 2: Перетворюємо дані...
Крок 3: Виводимо результат: JAVA
Крок 4: Усе завершено!

Зверніть увагу:
Кожен наступний крок починається лише після завершення попереднього. Це дозволяє будувати справжні «конвеєри» обробки даних.

4. Асинхронні варіанти: thenApplyAsync, thenAcceptAsync, thenRunAsync

За замовчуванням обробники (thenApply, thenAccept, thenRun) виконуються в тому ж потоці, у якому завершилося попереднє завдання. Іноді це не дуже зручно — якщо обробка важка, краще винести її в окремий потік.

Для цього придумані асинхронні версії:

  • thenApplyAsync
  • thenAcceptAsync
  • thenRunAsync

У чому різниця?

  • Без Async: обробник може виконатися в тому ж потоці, що й попереднє завдання (наприклад, якщо завдання було завершено в ForkJoinPool, обробник — там само).
  • З Async: обробник гарантовано буде виконано в іншому потоці з ForkJoinPool (або вашим Executor).

Приклад: порівняймо звичайний і асинхронний обробник

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    System.out.println("Завантаження... [" + Thread.currentThread().getName() + "]");
    return "Hello";
});

future.thenApply(result -> {
    System.out.println("thenApply: [" + Thread.currentThread().getName() + "]");
    return result + " World";
});

future.thenApplyAsync(result -> {
    System.out.println("thenApplyAsync: [" + Thread.currentThread().getName() + "]");
    return result + " Async World";
});

Типове виведення:

Завантаження... [ForkJoinPool.commonPool-worker-1]
thenApply: [ForkJoinPool.commonPool-worker-1]
thenApplyAsync: [ForkJoinPool.commonPool-worker-2]

Висновок:
Асинхронний обробник виконується в іншому потоці.

Коли використовувати Async-методи?

  • Якщо обробка ресурсоємна (наприклад, складні обчислення, робота з мережею).
  • Якщо не хочете блокувати потік, у якому завершилося попереднє завдання.
  • Якщо хочете явно керувати потоками (наприклад, передати свій Executor як другий аргумент).

5. Корисні нюанси

Таблиця: порівняння методів thenApply, thenAccept, thenRun

Метод Використовує результат? Повертає значення? Для чого використовувати
thenApply
Так Так Перетворення результату
thenAccept
Так Ні Побічні ефекти (виведення, логування)
thenRun
Ні Ні Просто дія після завершення завдання
thenApplyAsync
Так Так Те саме, але в іншому потоці
thenAcceptAsync
Так Ні Те саме, але в іншому потоці
thenRunAsync
Ні Ні Те саме, але в іншому потоці

Питання: як будувати довгі ланцюжки?

Можна викликати методи один за одним, як конструктор LEGO:

CompletableFuture.supplyAsync(() -> "42")
    .thenApply(Integer::parseInt)
    .thenApply(x -> x * 2)
    .thenAccept(x -> System.out.println("Результат: " + x));

Виведення:

Результат: 84

Кожен наступний крок отримує результат попереднього, може його змінити або просто використати.

6. Типові помилки під час роботи з thenApply, thenAccept, thenRun

Помилка № 1: Плутанина в типах значень, що повертаються.
thenApply має повертати значення, яке піде далі по ланцюжку. Якщо ви випадково використовуєте thenApply, але не повертаєте результат, наступна операція отримає null (або взагалі не скомпілюється). Для побічних ефектів використовуйте thenAccept або thenRun.

Помилка № 2: Спроба використати результат у thenRun.
Усередині thenRun немає доступу до результату попереднього завдання. Якщо ви хочете використати результат, обирайте thenApply або thenAccept.

Помилка № 3: Блокування головного потоку.
Якщо ви викликаєте get() або join() у головному потоці, ви втрачаєте всі переваги асинхронності: потік чекатиме завершення завдання, як у старому доброму синхронному коді. Краще використовувати неблокувальні ланцюжки і колбеки.

Помилка № 4: Необроблення помилок.
Якщо у ланцюжку виникає виняток, а ви не додали обробник (exceptionally, handle, whenComplete), він «загубиться», і задача може завершитися з помилкою, яку ви не побачите. Завжди обробляйте помилки у ланцюжках.

Помилка № 5: Неочікуване виконання в іншому потоці.
Асинхронні методи (thenApplyAsync тощо) можуть виконуватися в іншому потоці. Якщо ви звертаєтеся до змінних, не захищених від багатопотокового доступу, можуть виникнути гонки даних.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ