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

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

Общее описание и сигнатура

Метод Stream.forEachOrdered является терминальной операцией Java Stream API, доступной начиная с Java 8. Назначение метода - применять заданный Consumer<? super T> ко всем элементам потока с соблюдением порядка обхода (encounter order) тогда, когда он определён. Метод не возвращает значение (тип void).

Сигнатура для объектного потока:

void forEachOrdered(Consumer<? super T> action)

Для примитивных потоков имеются аналогичные перегрузки:

void forEachOrdered(IntConsumer action)
void forEachOrdered(LongConsumer action)
void forEachOrdered(DoubleConsumer action)

Поведение и использование:

  • Если поток является упорядоченным (например, List.stream()), действие будет применяться в порядке элементов источника.
  • Если поток параллельный, forEachOrdered гарантирует применение в порядке обхода, но это может снизить параллельную производительность из-за синхронизации вывода порядка.
  • Если поток помечен как unordered (например, вызовом unordered()), порядок не гарантируется, и реализация может применять действие в любом порядке.
  • Метод является терминальной операцией: после его вызова поток считается использованным и повторный доступ к нему приведет к исключению.

Исключения и пограничные случаи:

  • Если передать null в качестве action, будет брошено NullPointerException.
  • Если действие выбрасывает unchecked-исключение, оно пробрасывается вызывающему. Если поток параллельный, может наблюдаться агрегация исключений в зависимости от реализации.
  • Метод не возвращает результат для дальнейшей композиции; для возвращаемых результатов следует использовать collect или reduce.

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

Пример 1: последовательный поток, простой вывод в порядке элементов.

List<String> list = List.of("a", "b", "c");
list.stream().forEachOrdered(System.out::print);
abc

Пример 2: параллельный поток - сохранение исходного порядка при выводе.

List<Integer> nums = List.of(1,2,3,4);
nums.parallelStream().forEachOrdered(n -> System.out.print(n + " "));
1 2 3 4 

Пример 3: поток без порядка - порядок не гарантируется.

List<Integer> nums = List.of(1,2,3,4);
nums.stream().unordered().parallel().forEachOrdered(n -> System.out.print(n + " "));
(порядок может быть отличным от 1 2 3 4, например 3 1 4 2)

Пример 4: примитивный поток (IntStream).

IntStream.range(1, 5).forEachOrdered(System.out::print);
1234

Похожие методы в Java и отличия

Краткий обзор альтернатив и выбор в зависимости от задачи:

  • forEach: выполняет действие для каждого элемента, но не гарантирует порядок на параллельных потоках. Предпочтителен при необходимости максимальной скорости и когда порядок не важен.
  • collect: собирает элементы в коллекцию, затем можно обработать результат в нужном порядке. Уместен при необходимости агрегировать или возвращать значение.
  • reduce: применяется для свертки элементов в одно значение без побочных эффектов. Подходит для функциональных вычислений без мутаций.
  • Iterator.forEachRemaining: позволяет итеративно обработать элементы с сохранением порядка, при этом контроль над итерацией у вызывающего.
  • peek: промежуточная операция для отладки, не следует полагаться на её вызовы как на гарантированный итоговый обход.

При выборе между forEach и forEachOrdered следует соотнести требование к порядку и требования к производительности. Для сохранения порядка в параллельной обработке выбирается forEachOrdered, при допустимости любого порядка - forEach или параллельные collect-операции.

Типичные ошибки и примеры

Пошаговые типичные проблемы при использовании forEachOrdered.

Передача null

List<String> list = List.of("a");
list.stream().forEachOrdered(null);
Exception in thread "main" java.lang.NullPointerException
    at java.base/java.util.Objects.requireNonNull(Objects.java:...) ...

Повторное использование потока

Stream<Integer> s = Stream.of(1,2);
s.forEachOrdered(System.out::print);
s.forEachOrdered(System.out::print);
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed

Побочные эффекты с параллельностью

List<Integer> list = Collections.synchronizedList(new ArrayList<>());
IntStream.range(0,1000).parallel().forEachOrdered(i -> list.add(i));
System.out.println(list.size());
1000

Если убрать synchronizedList и использовать обычный ArrayList, возможны непредсказуемые результаты или ConcurrentModificationException. Главная ошибка - применение небезопасных для потоков структур без защиты.

Ожидание возвращаемого значения

int sum = Stream.of(1,2,3).forEachOrdered(System.out::print);
(код не компилируется - forEachOrdered возвращает void)

При попытке получить результат следует использовать collect или reduce.

История и изменения

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

Стоит учитывать, что оптимизации могут зависеть от характеристик Spliterator источника (например, CONCURRENT, ORDERED, SIZED). Для специфичных источников поведения производительности могут меняться в разных версиях JVM, но контракт метода остался прежним.

Расширенные и неочевидные сценарии

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

Пример java
List<Integer> ids = IntStream.range(1,6).boxed().collect(Collectors.toList());
ids.parallelStream().forEachOrdered(id -> {
    try { Thread.sleep(100 * (6 - id)); } catch (InterruptedException ignored) {}
    System.out.print(id + " ");
});
1 2 3 4 5 

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

2. Использование с бесконечным потоком и limit

Пример java
Stream.iterate(1, n -> n + 1)
      .parallel()
      .limit(5)
      .forEachOrdered(System.out::println);
1
2
3
4
5

Комбинация limit и forEachOrdered может потребовать от реализации синхронизации для определения первых N элементов.

3. Собирающий накопитель через побочный эффект с сохранением порядка

Пример java
List<String> out = Collections.synchronizedList(new ArrayList<>());
Stream.of("a","b","c").parallel().forEachOrdered(out::add);
System.out.println(out);
[a, b, c]

Без synchronizedList порядок может сохраниться, но безопасность потоков не гарантируется. Для безопасного и детерминированного сбора предпочтительнее collect с соответствующим Collector.

4. Логирование с гарантией упорядоченности в конкурентной среде

Пример java
ExecutorService ex = Executors.newFixedThreadPool(4);
List<Integer> data = IntStream.range(1,9).boxed().collect(Collectors.toList());
try {
    data.parallelStream().forEachOrdered(i -> {
        // имитация задачи, результаты логируются в порядке источника
        try { Thread.sleep(50 * (i % 3)); } catch (InterruptedException ignored) {}
        System.out.println(Thread.currentThread().getName() + ":" + i);
    });
} finally {
    ex.shutdown();
}
(строки будут выводиться в порядке 1..8, потоки могут отличаться)
pool-1-thread-1:1
pool-1-thread-3:2
...
pool-1-thread-2:8

Полезно при формировании логов, где важна последовательность записей, но обработка может быть распараллелена.

5. Обработка исключений внутри действия

Пример java
Stream.of(1,0,2).forEachOrdered(i -> {
    try {
        System.out.println(10 / i);
    } catch (ArithmeticException e) {
        System.out.println("div0 at " + i);
    }
});
10
div0 at 0
5

Исключения внутри consumer можно обрабатывать локально, иначе они прерывают выполнение и пробрасываются вверх.

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

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

JavaScript (Array)

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

В JS порядок выполнения синхронного forEach всегда сохраняется. В отличие от Java, нет встроенной параллельной версии коллекций.

Python

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

В Python порядок последовательный. Для параллельной обработки используются модули concurrent.futures, map в них не гарантирует порядок без дополнительных мер.

PHP

array_walk([1,2,3], function($v){ echo $v.PHP_EOL; });
1
2
3

Поведение последовательное; параллелизм редко встроен в стандартные массивные операции.

C#

new[]{1,2,3}.AsParallel().ForAll(Console.WriteLine);
(порядок может отличаться, например 2 1 3)

В C# PLINQ метод ForAll выполняет действие параллельно без сохранения порядка; для сохранения порядка можно использовать AsOrdered().

Go

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

Go выполняет цикл в порядке, но параллелизм достигается явно через горутины, и тогда порядок теряется, если не синхронизировать.

Kotlin

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

Kotlin использует похожую модель на Java; при Sequence и Stream-подобных операциях порядок сохраняется, есть средства для параллельной обработки через корутины или Java-параллелизм.

Основное отличие Java forEachOrdered от перечисленных методов в других языках - встроённая гарантия порядка при параллельной обработке потоков. Во многих языках поддержка параллельного последовательного обхода требует явной синхронизации.

джава Stream.forEachOrdered function comments

En
Stream.forEachOrdered Performs an action for each element of the stream, guaranteeing order in sequential streams