JavaRush /Курсы /JAVA 25 SELF /Методы forEach, peek: побочные действия

Методы forEach, peek: побочные действия

JAVA 25 SELF
30 уровень , 3 лекция
Открыта

1. Метод forEach: финальное действие над элементами

Перед тем как нырять в детали, давайте ещё раз вспомним, что такое терминальные и промежуточные операции потока.

  • Промежуточные операции (например, filter, map, distinct, peek) — возвращают новый поток и обычно не выполняют действий до вызова терминальной операции.
  • Терминальные операции (например, forEach, collect, count, anyMatch) — запускают обработку элементов потока и возвращают результат (или ничего не возвращают, как forEach).

После терминальной операции поток считается закрытым, и к нему больше нельзя применять другие операции. Это как пытаться доесть уже съеденное мороженое — не получится: поток уже «использован».

Теперь познакомимся с двумя важными методами для работы с побочными действиями: forEach и peek.

Что делает forEach?

forEach — это терминальная операция потока, которая выполняет заданное действие для каждого элемента потока. Обычно используется для вывода на экран, записи в лог, подсчёта статистики и других побочных эффектов (side effects).

Сигнатура метода:

void forEach(Consumer<? super T> action)

Consumer<T> — это функциональный интерфейс, принимающий один аргумент и ничего не возвращающий (например, System.out::println).

Пример: Вывести все элементы списка

Допустим, у нас есть список имён пользователей:

List<String> users = List.of("Anna", "Boris", "Alex", "Alina", "Dmitry");

Вывести всех пользователей на экран с помощью Stream API очень просто:

users.stream().forEach(System.out::println);

Результат:

Anna
Boris
Alex
Alina
Dmitry

Можно использовать и лямбда-выражение:

users.stream().forEach(name -> System.out.println("Пользователь: " + name));

Результат:

Пользователь: Anna
Пользователь: Boris
Пользователь: Alex
Пользователь: Alina
Пользователь: Dmitry

Важно: forEach завершает поток

После вызова forEach поток «закрывается». Нельзя продолжать цепочку:

users.stream()
     .filter(name -> name.startsWith("A"))
     .forEach(System.out::println)
     .map(String::toUpperCase); // Ошибка! Поток уже закрыт.

Попытка вызвать что-то после forEach приведёт к ошибке компиляции: терминальная операция возвращает void, а не новый поток.

2. Метод peek: подглядываем, но не вмешиваемся

peek — это промежуточная операция. Она позволяет выполнить действие для каждого элемента на определённом этапе обработки, не изменяя сам элемент и не завершая поток.

Сигнатура метода:

Stream<T> peek(Consumer<? super T> action)
  • peek возвращает новый поток, в котором для каждого элемента будет выполнено действие action.
  • Обычно используется для отладки, логирования или мониторинга состояния потока.

Пример: Логирование после фильтрации

List<String> users = List.of("Anna", "Boris", "Alex", "Alina", "Dmitry");

List<Integer> nameLengths = users.stream()
    .filter(name -> name.startsWith("A"))
    .peek(name -> System.out.println("Прошёл фильтр: " + name))
    .map(String::length)
    .collect(Collectors.toList());

Результат в консоли:

Прошёл фильтр: Anna
Прошёл фильтр: Alex
Прошёл фильтр: Alina

Содержимое nameLengths:

[4, 4, 5]

Где удобно использовать peek?

  • Для отладки цепочки операций: посмотреть, что происходит на каждом этапе.
  • Для сбора статистики (например, подсчёта количества элементов).
  • Для логирования данных на промежуточных этапах.

Важно: peek не должен использоваться для изменения элементов потока. Для преобразований есть map. peek — это «подсмотреть», а не «вмешаться».

3. forEach vs peek: в чём разница?

Метод Тип операции Когда применяется Можно продолжать цепочку? Для чего лучше подходит
forEach
Терминальная В самом конце обработки потока Нет Финальные действия (вывод, логирование, запись в БД)
peek
Промежуточная В середине цепочки операций Да Отладка, промежуточное логирование, подсчёт

Пример: разница в использовании

// Пример с forEach
users.stream()
    .filter(name -> name.startsWith("A"))
    .map(String::toUpperCase)
    .forEach(System.out::println); // Здесь поток завершается

// Пример с peek
users.stream()
    .filter(name -> name.startsWith("A"))
    .peek(name -> System.out.println("Фильтр прошёл: " + name))
    .map(String::toUpperCase)
    .collect(Collectors.toList()); // Можно продолжать цепочку

Важно помнить

  • forEach — это точка невозврата: после неё с потоком больше ничего не сделать.
  • peek не гарантирует выполнение действий, если не будет вызвана терминальная операция. Если написать только цепочку из промежуточных операций — ничего не произойдёт.

4. Неочевидные моменты: forEach не всегда лучший выбор!

Почему не стоит использовать forEach для изменения коллекций?

Многие новички пытаются с помощью forEach изменять элементы коллекции или саму коллекцию (например, удалять элементы). Но это плохая практика: потоки не предназначены для модификации исходных коллекций.

Пример неправильного использования:

List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Alex"));
names.stream().forEach(name -> {
    if (name.startsWith("A")) {
        names.remove(name); // Может привести к ConcurrentModificationException!
    }
});

Результат: ошибка времени выполнения — нельзя изменять коллекцию во время обхода потока (ConcurrentModificationException).

Для чего всё-таки использовать forEach?

  • Для вывода на экран (например, печать отчёта).
  • Для записи в лог.
  • Для вызова внешних сервисов (например, отправка e-mail).
  • Для сбора статистики (например, увеличение счётчика).

5. Ещё раз про peek: только для отладки!

Очень хочется использовать peek для изменения элементов, например, увеличить возраст пользователя:

users.stream()
    .peek(user -> user.setAge(user.getAge() + 1)) // Плохо!
    .collect(Collectors.toList());

Почему плохо?

  • Это нарушает декларативность и чистоту Stream API.
  • Такой код становится трудно поддерживать и тестировать.
  • Побочные эффекты в промежуточной операции могут вести к неочевидным багам.

Лучше использовать map для преобразования данных:

List<User> olderUsers = users.stream()
    .map(user -> new User(user.getName(), user.getAge() + 1))
    .collect(Collectors.toList());

Схема: разница между forEach и peek

users.stream()
    .filter(...)         // промежуточная операция
    .peek(...)           // промежуточная операция, "подсматриваем"
    .map(...)            // промежуточная операция
    .forEach(...)        // терминальная операция, "делаем действие"

Пояснение:
— Всё, что до forEach, можно комбинировать, переставлять, добавлять.
— После forEach поток закрыт.

6. Типичные ошибки при работе с forEach и peek

Ошибка №1: Использовать peek для изменения данных. peek предназначен только для наблюдения, а не для изменения элементов потока. Для преобразований используйте map.

Ошибка №2: Ожидать, что peek всегда выполнится. peek выполняется только если после него есть терминальная операция (collect, forEach, count и т.д.). Без терминальной операции ничего не произойдёт.

Ошибка №3: Пытаться продолжить поток после forEach. forEach — терминальная операция. После неё нельзя вызывать другие методы потока.

Ошибка №4: Модифицировать коллекцию внутри forEach. Изменять исходную коллекцию (удалять или добавлять элементы) во время обхода через forEach — прямой путь к ConcurrentModificationException.

Ошибка №5: Использовать forEach вместо collect для сбора результата. Если вы хотите собрать элементы в новую коллекцию, используйте collect(Collectors.toList()), а не forEach с ручным добавлением. Это нарушает декларативность и может привести к ошибкам в многопоточных сценариях.

1
Задача
JAVA 25 SELF, 30 уровень, 3 лекция
Недоступна
Вызов Учеников к Доске 👩‍🏫
Вызов Учеников к Доске 👩‍🏫
1
Задача
JAVA 25 SELF, 30 уровень, 3 лекция
Недоступна
Отладка Потока Данных в Умном Доме 🏠
Отладка Потока Данных в Умном Доме 🏠
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ