Вопрос 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 блокирует поток? — Нет, выполняется callback-ом при завершении 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. Что такое блокирующий код и как его отличить от неблокирующего]]