Автор оригинала: Michael Krimgen.
Разница между Collection.stream ().forEach() и Collection.forEach()
1. Введение
Существует несколько вариантов итерации коллекции на Java. В этом коротком учебнике мы посмотрим на два похожих подхода – Collection.stream ().forEach() и Collection.forEach() .
В большинстве случаев, оба даст те же результаты, однако, Есть некоторые тонкие различия, которые мы будем смотреть.
2. Обзор
Во-первых, давайте создадим список, чтобы итерировать более:
Listlist = Arrays.asList("A", "B", "C", "D");
Самый простой способ заключается в использовании расширенного цикла:
for(String s : list) { //do something with s }
Если мы хотим использовать Java в функциональном стиле, мы также можем использовать forEach () . Мы можем сделать это непосредственно на сборе:
Consumerconsumer = s -> { System.out::println }; list.forEach(consumer);
Или мы можем позвонить forEach () на потоке коллекции:
list.stream().forEach(consumer);
Обе версии будут итерировать по списку и печатать все элементы:
ABCD ABCD
В этом простом случае, это не имеет значения, forEach () мы используем.
3. Приказ о приведении в исполнение
Collection.forEach() использует итератор коллекции (если он указан). Это означает, что порядок обработки элементов определен. В отличие от этого, порядок обработки Collection.stream ().forEach() не определена.
В большинстве случаев, это не имеет значения, какие из двух мы выбираем.
3.1. Параллельные потоки
Параллельные потоки позволяют выполнять поток несколькими потоками, и в таких ситуациях порядок выполнения не определен. Java требует только завершения всех потоков перед любой операцией терминала, например Коллекционеры.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{ @Override public Iterator iterator() { int startIndex = this.size() - 1; List list = this; Iterator it = new Iterator () { 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 () непосредственно на сборе, а затем на потоке:
ListmyList = new ReverseList(); myList.addAll(list); myList.forEach(System.out::print); System.out.print(" "); myList.stream().forEach(System.out::print);
Мы получаем разные результаты:
DCBA ABCD
Причина различных результатов заключается в том, что forEach () используется непосредственно в списке использует пользовательский итератор, в то поток ().forEach() просто берет элементы один за другим из списка, игнорируя итератор.
4. Модификация коллекции
Многие коллекции (например, ArrayList или HashSet ) не должны быть структурно изменены при итерации над ними. Если элемент удален или добавлен во время итерации, мы получим Параллельноесовременная исключение.
Кроме того, коллекции предназначены для быстрого сверша, что означает, что исключение выбрасывается, как только есть модификация.
Аналогичным образом, мы получим Параллельноемодификация исключение при добавление или удалении элемента во время выполнения конвейера потока. Однако исключение будет брошено позже.
Еще одно тонкое различие между этими двумя forEach () методы заключается в том, что Java явно позволяет изменять элементы с помощью итератора. Потоки, напротив, должны не вмешиваться.
Рассмотрим удаление и изменение элементов более подробно.
4.1. Удаление элемента
Давайте определим операцию, которая удаляет последний элемент (“D”) нашего списка:
ConsumerremoveElement = s -> { System.out.println(s + " " + list.size()); if (s != null && s.equals("A")) { list.remove("D"); } };
Когда мы итерировать по списку, последний элемент удаляется после первого элемента (“А”) печатается:
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)
Давайте посмотрим, что произойдет, если мы используем поток ().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 не гарантирует, что ПараллельноМодификацияИсключение брошен на всех. Это означает, что мы никогда не должны писать программу, которая зависит от этого исключения.
4.2. Изменение элементов
Мы можем изменить элемент во время итерации списка:
list.forEach(e -> { list.set(3, "E"); });
Однако, в то время как нет никаких проблем с этим с помощью Collection.forEach() или поток ().forEach() , Java требует, чтобы операция на потоке не вмешивалась. Это означает, что элементы не должны быть изменены во время выполнения конвейера потока.
Причина этого заключается в том, что поток должен облегчить параллельное выполнение. Здесь изменение элементов потока может привести к неожиданному поведению.
5. Заключение
В этой статье мы видели несколько примеров, которые показывают тонкие различия между Collection.forEach() и Collection.stream ().forEach() .
Тем не менее, важно отметить, что все примеры, показанные выше, тривиальны и предназначены только для сравнения двух способов итерации по сравнению с коллекцией. Мы не должны писать код, правильность которого зависит от показанного поведения.
Если нам не нужен поток, а только мы хотим итерировать коллекцию, первым выбором должно быть использование forEach() непосредственно на коллекцию.
Исходный код для примеров в этой статье доступен на GitHub.