Перейти к основному содержимому

Разница между Collection.stream().forEach() и Collection.forEach()

· 5 мин. чтения

1. Обзор

В Java есть несколько вариантов перебора коллекции. В этом коротком руководстве мы рассмотрим два похожих подхода — Collection.stream().forEach() и Collection.forEach() .

В большинстве случаев оба дадут одинаковые результаты, но мы рассмотрим некоторые тонкие различия.

2. Простой список

Во-первых, давайте создадим список для повторения:

List<String> list = Arrays.asList("A", "B", "C", "D");

Самый простой способ — использовать расширенный цикл for:

for(String s : list) {
//do something with s
}

Если мы хотим использовать Java в функциональном стиле, мы также можем использовать forEach() .

Мы можем сделать это непосредственно в коллекции:

Consumer<String> consumer = s -> { System.out::println }; 
list.forEach(consumer);

Или мы можем вызвать forEach() для потока коллекции:

list.stream().forEach(consumer);

Обе версии будут перебирать список и печатать все элементы:

ABCD ABCD

В этом простом случае не имеет значения, какую функцию forEach() мы используем.

3. Порядок исполнения

Collection.forEach() использует итератор коллекции (если он указан), поэтому определяется порядок обработки элементов. Напротив, порядок обработки Collection.stream().forEach() не определен.

В большинстве случаев не имеет значения, какой из двух мы выберем.

3.1. Параллельные потоки

Параллельные потоки позволяют нам выполнять поток в несколько потоков, и в таких ситуациях порядок выполнения не определен. Java требует только завершения всех потоков перед вызовом какой-либо терминальной операции, такой как Collectors.toList() .

Давайте рассмотрим пример, в котором мы сначала вызываем forEach() непосредственно для коллекции, а затем — для параллельного потока:

list.forEach(System.out::print);
System.out.print(" ");
list.parallelStream().forEach(System.out::print);

Если мы запустим код несколько раз, то увидим, что list.forEach() обрабатывает элементы в порядке вставки, а list.parallelStream().forEach() выдает разные результаты при каждом запуске.

Вот один из возможных выходов:

ABCD CDBA

А это другое:

ABCD DBCA

3.2. Пользовательские итераторы

Давайте определим список с пользовательским итератором для перебора коллекции в обратном порядке:

class ReverseList extends ArrayList<String> {

@Override
public Iterator<String> iterator() {

int startIndex = this.size() - 1;
List<String> list = this;

Iterator<String> it = new Iterator<String>() {

private int currentIndex = startIndex;

@Override
public boolean hasNext() {
return currentIndex >= 0;
}

@Override
public String next() {
String next = list.get(currentIndex);
currentIndex--;
return next;
}

@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
return it;
}
}

Затем мы снова пройдемся по списку с помощью forEach() непосредственно в коллекции, а затем в потоке:

List<String> myList = new ReverseList();
myList.addAll(list);

myList.forEach(System.out::print);
System.out.print(" ");
myList.stream().forEach(System.out::print);

И получаем разные результаты:

DCBA ABCD

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

4. Модификация коллекции

Многие коллекции (например , ArrayList или HashSet ) не должны изменяться структурно при их повторении. Если элемент будет удален или добавлен во время итерации, мы получим исключение ConcurrentModification .

Кроме того, коллекции рассчитаны на быстрый сбой, а это означает, что исключение генерируется, как только происходит модификация.

Точно так же мы получим исключение ConcurrentModification при добавлении или удалении элемента во время выполнения потокового конвейера. Однако исключение будет выброшено позже.

Еще одно тонкое различие между двумя методами forEach() заключается в том, что Java явно позволяет изменять элементы с помощью итератора. Потоки, напротив, не должны мешать друг другу.

Рассмотрим удаление и изменение элементов более подробно.

4.1. Удаление элемента

Давайте определим операцию, которая удаляет последний элемент («D») нашего списка:

Consumer<String> removeElement = s -> {
System.out.println(s + " " + list.size());
if (s != null && s.equals("A")) {
list.remove("D");
}
};

Когда мы перебираем список, последний элемент удаляется после того, как напечатан первый элемент («A»):

list.forEach(removeElement);

Поскольку forEach() работает без сбоев, мы останавливаем итерацию и видим исключение перед обработкой следующего элемента :

A 4
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList.forEach(ArrayList.java:1252)
at ReverseList.main(ReverseList.java:1)

Давайте посмотрим, что произойдет, если вместо этого мы используем stream().forEach() :

list.stream().forEach(removeElement);

Здесь мы продолжаем перебирать весь список, прежде чем увидим исключение :

A 4
B 3
C 3
null 3
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1380)
at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
at ReverseList.main(ReverseList.java:1)

Однако Java не гарантирует, что исключение ConcurrentModificationException будет выброшено вообще. Это означает, что мы никогда не должны писать программу, зависящую от этого исключения.

4.2. Изменение элементов

Мы можем изменить элемент во время итерации по списку:

list.forEach(e -> {
list.set(3, "E");
});

Но хотя нет проблем с выполнением этого с помощью Collection.forEach() или stream().forEach() , Java требует, чтобы операция над потоком не мешала. Это означает, что элементы не должны изменяться во время выполнения потокового конвейера.

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

5. Вывод

В этой статье мы видели несколько примеров, которые показывают тонкие различия между Collection.forEach() и Collection.stream().forEach() .

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

Если нам не нужен поток, а нужно только перебирать коллекцию, первым выбором должно быть использование forEach() непосредственно в коллекции.

Исходный код примеров из этой статьи доступен на GitHub .