Питання 5 · Розділ 19

Що роблять методи thenAccept() і thenRun()

Обидва методи використовуються для завершення ланцюжка CompletableFuture:

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Обидва методи використовуються для завершення ланцюжка CompletableFuture:

  • thenAccept() — приймає результат і споживає його (не повертає нічого)
  • thenRun() — просто виконує дію, не знаючи про результат
// thenAccept — отримує результат
CompletableFuture.supplyAsync(() -> "Hello")
    .thenAccept(s -> System.out.println("Got: " + s));  // Got: Hello

// thenRun — не отримує результат
CompletableFuture.supplyAsync(() -> "Hello")
    .thenRun(() -> System.out.println("Done!"));  // Done!

Проста аналогія:

  • thenAccept — отримати посилку і відкрити її
  • thenRun — просто знати, що посилка доставлена

🟡 Middle Level

Детальне порівняння

**thenAccept — Consumer:**

CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> "Hello");

// Отримуємо результат
cf.thenAccept(result -> {
    System.out.println("Result: " + result);
    saveToDatabase(result);
});

// Тип: CompletableFuture<Void> — не можна продовжити ланцюжок зі значенням

thenRun — Runnable:

CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> "Hello");

// Просто дія після завершення
cf.thenRun(() -> {
    System.out.println("Task completed");
    notifyUser();
});

Коли що використовувати

thenAccept:

// Коли потрібен результат
fetchDataAsync()
    .thenAccept(data -> {
        process(data);
        saveToCache(data);
    });

thenRun:

// Коли результат не важливий
sendEmailAsync(email)
    .thenRun(() -> log.info("Email sent"));

// Або для side effects
saveToDatabase(entity)
    .thenRun(() -> metrics.increment("saved"));

Типові помилки

  1. Спроба отримати результат у thenRun: ```java CompletableFuture cf = CompletableFuture.supplyAsync(() -> "Hello");

// ❌ thenRun не отримує результат cf.thenRun(() -> { // System.out.println(result); // result недоступний! System.out.println(“Done”); });

// ✅ thenAccept отримує результат cf.thenAccept(result -> System.out.println(result));


---

## 🔴 Senior Level

### Internal Implementation

**thenApply vs thenAccept vs thenRun:**
```java
public <U> CompletableFuture<U> thenApply(Function<T, U> fn) {
    // fn: T -> U
    // Повертає CompletableFuture<U>
}

public CompletableFuture<Void> thenAccept(Consumer<T> action) {
    // action: T -> void
    // Повертає CompletableFuture<Void>
}

public CompletableFuture<Void> thenRun(Runnable action) {
    // action: () -> void
    // Повертає CompletableFuture<Void>
}

Void result:

// thenAccept і thenRun повертають CompletableFuture<Void>
// Але можна продовжити ланцюжок:
cf.thenAccept(result -> process(result))
  .thenRun(() -> notify());  // OK

cf.thenRun(() -> log.info("Done"))
  .thenAccept(v -> { });  // v — Void, марно

Архітектурні Trade-offs

Метод Отримує результат Повертає Use case
thenApply ✅ T U Трансформація
thenAccept ✅ T Void Споживання
thenRun Void Side effect

Edge Cases

1. Exception handling:

// Виключення в thenAccept:
cf.thenAccept(result -> {
    throw new RuntimeException("Error processing");
}).exceptionally(ex -> {
    // Спіймає виключення
    return null;
});

// thenRun теж ловить виключення
cf.thenRun(() -> {
    throw new RuntimeException("Error");
}).exceptionally(ex -> {
    // Спіймає
    return null;
});

2. Async версії:

// thenAcceptAsync — виконується у ForkJoinPool
cf.thenAcceptAsync(result -> process(result));

// З своїм Executor
cf.thenAcceptAsync(result -> process(result), executor);

// thenRunAsync — аналогічно
cf.thenRunAsync(() -> notify(), executor);

Продуктивність

thenApply: ~5 ns (функція)
thenAccept: ~5 ns (consumer)
thenRun: ~3 ns (runnable — немає доступу до T)

thenAcceptAsync / thenRunAsync:
- + ~1μs на переключення потоку

Production Experience

Logging and metrics:

@Service
public class OrderService {
    public CompletableFuture<Order> createOrder(OrderRequest req) {
        return orderRepository.saveAsync(req)
            .thenApply(order -> {
                metrics.increment("orders.created");
                eventPublisher.publish(new OrderCreatedEvent(order));
                return order;  // thenApply повертає результат — order в scope
            });
    }

    // thenRun для side effects
    public CompletableFuture<Void> sendNotification(Order order) {
        return notificationClient.sendAsync(order)
            .thenRun(() -> log.info("Notification sent for order {}", order.id()));
    }
}

Best Practices

// ✅ thenAccept для споживання результату
cf.thenAccept(result -> process(result));

// ✅ thenRun для side effects
cf.thenRun(() -> metrics.increment("done"));

// ✅ Async версії з Executor
cf.thenAcceptAsync(result -> process(result), executor);

// ❌ thenRun коли потрібен результат
// ❌ thenAccept коли результат не потрібен
// ❌ Блокуючі операції в thenAccept/thenRun

Коли НЕ використовувати thenRun

Коли результат попереднього етапу потрібен для подальшої обробки. thenRun = “зроби щось після, але результат мені не важливий”.

// ❌ thenRun — order недоступний
orderRepository.saveAsync(req)
    .thenRun(() -> notifyUser(order.id()));  // compilation error: order not found

// ✅ thenAccept — order доступний
orderRepository.saveAsync(req)
    .thenAccept(order -> notifyUser(order.id()));

🎯 Шпаргалка для співбесіди

Обов’язково знати:

  • thenAccept(Consumer) — отримує результат T, повертає CompletableFuture
  • thenRun(Runnable) — НЕ отримує результат, повертає CompletableFuture
  • thenAccept для споживання результату, thenRun для side effects (логування, метрики)
  • Обидва мають Async версії з переключенням потоку
  • thenApply vs thenAccept: thenApply повертає U, thenAccept повертає Void

Часті уточнюючі питання:

  • Коли thenAccept, а коли thenRun? — thenAccept коли потрібен результат попереднього CF, thenRun коли результат не важливий
  • Чи можна продовжити ланцюжок після thenAccept? — Так, але наступна ланка отримує Void
  • thenAccept блокує потік? — Ні, виконується колбеком при завершенні CF
  • thenAccept vs thenApply — коли що? — thenApply для трансформації (повертає U), thenAccept для споживання (повертає Void)

Червоні прапорці (НЕ говорити):

  • «thenRun отримує результат попереднього CF» — thenRun використовує Runnable, без параметрів
  • «thenAccept завершує ланцюжок назавжди» — можна продовжити, але з Void
  • «thenAcceptAsync завжди кращий thenAccept» — Async додає ~1μs overhead, для легких операцій надмірний

Пов’язані теми:

  • [[4. У чому різниця між thenApply() і thenCompose()]]
  • [[6. Як обробляти виключення в ланцюжку CompletableFuture]]
  • [[11. У чому різниця між thenApply() і thenApplyAsync()]]
  • [[14. Що таке блокуючий код і як його відрізнити від неблокуючого]]