Stream.forEach: примеры (JAVA)

Практика применения forEach в потоках Java
Раздел: Потоки данных (Stream API) - терминальные операции
Stream.forEach(Consumer action): void

Описание Stream.forEach и его поведение

Метод Stream.forEach - терминальная операция потокового API Java, предназначенная для применения заданного действия к каждому элементу потока. Подпись метода в общем виде выглядит так: default void forEach(Consumer action). Для примитивных потоков существуют аналогичные методы: IntStream.forEach, LongStream.forEach, DoubleStream.forEach.

Основные свойства и сценарии использования:

  • Метод выполняет предоставленный Consumer для каждого элемента потока и возвращает void. Набор элементов обрабатывается в рамках одной конвейерной операции.
  • forEach - терминальная операция. После её вызова исходный поток считается потреблённым; повторный доступ к тому же потоку приведёт к ошибке.
  • Для последовательных потоков порядок обхода элементов обычно совпадает с последовательностью источника. Для параллельных потоков порядок выполнения не гарантируется; при необходимости сохранения порядка используется forEachOrdered.
  • Аргумент метода - объект, реализующий интерфейс Consumer<T>. Если передан null, генерируется NullPointerException. Если реализация Consumer генерирует исключение, оно пробрасывается вызывающему коду.
  • forEach не предназначен для выполнения операций, требующих короткого замыкания потока (short-circuit). Для таких случаев используются другие терминальные операции, например findFirst, anyMatch.
  • Параллельное использование forEach требует обеспечения потокобезопасности побочных эффектов. Если действие изменяет внешние структуры данных, рекомендуется использовать конкурентные коллекции или атомарные структуры.
  • Сложные операции трансформации и агрегации предпочтительнее выполнять через промежуточные операции и коллекторы, а не через побочные эффекты в forEach.

Возвращаемое значение: void. Возможные исключения: NullPointerException при null-аргументе, исключения, порожденные самим Consumer, и IllegalStateException при повторном использовании потока.

Короткие примеры использования

Примеры используют Java 8+ синтаксис. Код и пример вывода представлены отдельно.

1. Последовательный поток

import java.util.List;

List<Integer> list = List.of(1, 2, 3);
list.stream().forEach(System.out::println);
1
2
3

2. Параллельный поток - порядок неопределён

import java.util.stream.IntStream;

IntStream.rangeClosed(1, 6)
         .parallel()
         .forEach(i -> System.out.println(i));
(пример вывода, порядок может отличаться)
3
1
6
2
5
4

3. Побочный эффект: заполнение внешнего списка (не потокобезопасно для parallel)

import java.util.ArrayList;
import java.util.List;

List<Integer> target = new ArrayList<>();
List.of(1,2,3).stream().forEach(target::add);
System.out.println(target);
[1, 2, 3]

4. Null в качестве аргумента - NPE

List.of(1).stream().forEach(null);
Exception in thread "main" java.lang.NullPointerException
	at java.base/java.util.Objects.requireNonNull(Objects.java:255)
	at java.base/java.util.stream.Streams.lambda$noopClose$0(Streams.java:66)
	... 

5. Повторный вызов операций на потоке - IllegalStateException

var s = List.of(1,2,3).stream();
 s.forEach(System.out::println);
 s.forEach(System.out::println); // попытка повторного использования
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
	... 

Похожие возможности внутри Java и их особенности

  • forEachOrdered - сохраняет порядок элементов исходного потока даже в параллельном режиме, но может снизить производительность для параллельных вычислений.
  • peek - промежуточная операция для отладки и просмотра элементов без завершения конвейера; не гарантирует выполнение для всех элементов, если поток использует короткозамыкающие операции.
  • collect и toArray - предпочтительнее при необходимости собрать результаты без побочных эффектов; безопаснее и декларативнее.
  • reduce и агрегирующие методы - предназначены для чистых агрегаций без внешних побочных эффектов; лучше подходят для параллелизма.
  • Iterator.forEachRemaining - альтернатива при работе с итераторами коллекций вне Stream API.
  • Классический цикл for / enhanced for - простая и предсказуемая альтернатива, особенно при необходимости управления индексами или прерывания цикла.

Выбор зависит от требований: если требуется побочный эффект и порядок не важен - forEach; если важен порядок в параллельном режиме - forEachOrdered; если нужно собрать результат - collect.

Аналоги в других языках и отличия

JavaScript (Array.forEach, Set.forEach)

[1,2,3].forEach(x => console.log(x));
1
2
3

Отличие: JavaScript выполняет итерацию в основном потоке, нет встроенной параллельной версии. ForEach не ожидает промисы.

Python (цикл for, map, itertools)

for x in [1,2,3]:
    print(x)
1
2
3

Отличие: явный цикл предоставляет тот же контроль, map возвращает итератор. Для параллелизма используются multiprocessing или concurrent.futures.

PHP (foreach)

foreach ([1,2,3] as $x) { echo $x.PHP_EOL; }
1
2
3

C# (foreach, List.ForEach)

var list = new List<int>{1,2,3};
list.ForEach(Console.WriteLine);
1
2
3

Отличие: в C# есть Parallel.ForEach для параллельной обработки элементов.

Go (for range)

for _, v := range []int{1,2,3} {
    fmt.Println(v)
}
1
2
3

Отличие: для параллелизма используются горутины и каналы; порядок обработки можно нарушить при конкурентных независимых горутинах.

Kotlin (forEach)

listOf(1,2,3).forEach { println(it) }
1
2
3

Отличие: Kotlin использует похожую сигнатуру; на JVM поведение схоже с Java, но синтаксис компактнее.

SQL

В SQL нет прямого аналога. Обработка строк выполняется через SELECT и агрегирующие функции или через клиентский код, который итерирует результат.

Типичные ошибки при использовании forEach

  • NullPointerException при передаче null вместо действия. Пример:
List.of(1).stream().forEach(null);
java.lang.NullPointerException
  • IllegalStateException при повторном использовании потока после терминальной операции.
var s = List.of(1).stream();
s.forEach(System.out::println);
s.forEach(System.out::println);
java.lang.IllegalStateException: stream has already been operated upon or closed
  • ConcurrentModificationException при изменении коллекции во время её обхода без средств синхронизации или при использовании небезопасных структур в parallel stream.
var list = new ArrayList<>(List.of(1,2,3));
list.stream().forEach(i -> list.remove(i));
Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1043)
	... 
  • Непредсказуемость порядка при использовании параллельного потока для задач, где порядок важен. Решение - forEachOrdered или последовательный поток.
  • Проблемы с потокобезопасностью при побочных эффектах в параллельных потоках. Рекомендация - использовать конкурентные коллекции, атомарные типы, или собрать результаты через коллекторы.

Изменения в поведении и API

Метод forEach появился с введением Stream API в Java 8 и с тех пор остался частью API без изменений сигнатуры. Для работы с порядком был добавлен forEachOrdered в том же релизе. В последующих версиях Java добавлялись новые возможности Stream API (дополнительные методы, улучшения производительности), но сам forEach изменений не претерпел. Рекомендации по использованию параллельных стримов и потокобезопасности остаются актуальными.

Расширенные и редкие варианты применения

1. Параллельная обработка с сохранением порядка вывода

Пример java
import java.util.stream.IntStream;

IntStream.rangeClosed(1,10)
         .parallel()
         .forEachOrdered(i -> System.out.print(i + " "));
System.out.println();
1 2 3 4 5 6 7 8 9 10 

2. Безопасное накопление через ConcurrentLinkedQueue или ConcurrentHashMap

Пример java
import java.util.concurrent.ConcurrentLinkedQueue;

var q = new ConcurrentLinkedQueue<Integer>();
IntStream.rangeClosed(1,1000)
         .parallel()
         .forEach(q::add);
System.out.println(q.size());
1000

3. Использование AtomicInteger для подсчётов в parallel

Пример java
import java.util.concurrent.atomic.AtomicInteger;

AtomicInteger sum = new AtomicInteger();
IntStream.rangeClosed(1,1000)
         .parallel()
         .forEach(sum::addAndGet);
System.out.println(sum.get());
500500  // пример результата: сумма 1..1000

4. Обработка исключений внутри Consumer с агрегацией ошибок

Пример java
import java.util.ArrayList;
import java.util.List;

List<String> items = List.of("10","a","20");
List<Exception> errors = new ArrayList<>();
items.stream().forEach(s -> {
    try {
        int v = Integer.parseInt(s);
        System.out.println(v);
    } catch (Exception e) {
        synchronized (errors) { errors.add(e); }
    }
});
System.out.println("errors=" + errors.size());
10
20
errors=1

5. Использование Spliterator для более тонкого контроля перебора и параллелизма

Пример java
import java.util.Spliterator;
import java.util.stream.StreamSupport;

var list = List.of(1,2,3,4);
Spliterator<Integer> sp = list.spliterator();
StreamSupport.stream(sp, true)
             .forEach(System.out::println);
(возможный вывод в произвольном порядке)

6. Встраивание блокирующих операций в forEach и побочные эффекты на производительность

Пример java
import java.util.List;

List.of(1,2,3).parallel().forEach(i -> {
    try { Thread.sleep(100); } catch (InterruptedException e) {}
    System.out.println(i);
});
(вывод может быть параллельным; общая задержка меньше суммарной по сравнению с последовательным выполнением)

7. Подходы для логирования и мониторинга внутри потока

Пример java
List.of("a","b","c").stream()
    .peek(x -> System.out.println("before: " + x))
    .map(String::toUpperCase)
    .forEach(x -> System.out.println("after: " + x));
before: a
after: A
before: b
after: B
before: c
after: C

Примеры демонстрируют применение forEach для задач с побочными эффектами, безопасное накопление результатов в конкурентных структурах, а также риски при неправильном использовании в параллельном режиме.

джава Stream.forEach function comments

En
Stream.forEach Performs an action for each element of the stream