Рубрики
Без рубрики

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

Краткий и практический обзор разницы между Collection.stream ().forEach() и Collection.forEach().

Автор оригинала: Michael Krimgen.

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

1. Введение

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

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

2. Обзор

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

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

Самый простой способ заключается в использовании расширенного цикла:

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

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

Consumer 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 требует только завершения всех потоков перед любой операцией терминала, например Коллекционеры.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 () непосредственно на сборе, а затем на потоке:

List myList = 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”) нашего списка:

Consumer removeElement = 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.