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-операции.

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

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

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 от перечисленных методов в других языках - встроённая гарантия порядка при параллельной обработке потоков. Во многих языках поддержка параллельного последовательного обхода требует явной синхронизации.

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

Пошаговые типичные проблемы при использовании 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 можно обрабатывать локально, иначе они прерывают выполнение и пробрасываются вверх.

джава Stream.forEachOrdered function comments

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