Stream.forEach: примеры (JAVA)
Stream.forEach(Consumer super T> action): voidОписание Stream.forEach и его поведение
Метод Stream.forEach - терминальная операция потокового API Java, предназначенная для применения заданного действия к каждому элементу потока. Подпись метода в общем виде выглядит так: default void forEach(Consumer super T> 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. Параллельная обработка с сохранением порядка вывода
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
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
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 с агрегацией ошибок
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 для более тонкого контроля перебора и параллелизма
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 и побочные эффекты на производительность
import java.util.List;
List.of(1,2,3).parallel().forEach(i -> {
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println(i);
});
(вывод может быть параллельным; общая задержка меньше суммарной по сравнению с последовательным выполнением)
7. Подходы для логирования и мониторинга внутри потока
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 для задач с побочными эффектами, безопасное накопление результатов в конкурентных структурах, а также риски при неправильном использовании в параллельном режиме.