В чем разница между Collection.stream (). ForEach () и Collection.forEach ()?

287

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

collection.stream().forEach(System.out::println);
collection.forEach(System.out::println);
VladS
источник

Ответы:

288

Для простых случаев, таких как иллюстрированный, они в основном одинаковы. Тем не менее, есть ряд тонких различий, которые могут быть значительными.

Одна проблема с заказом. При Stream.forEachэтом порядок не определен . Это вряд ли произойдет с последовательными потоками, тем не менее, оно находится в пределах спецификации для Stream.forEachвыполнения в произвольном порядке. Это часто происходит в параллельных потоках. Напротив, Iterable.forEachвсегда выполняется в порядке итерации Iterable, если он указан.

Другая проблема связана с побочными эффектами. Действие , указанное в Stream.forEachобязан быть без вмешательства . (См. Документ пакета java.util.stream .) Iterable.forEachПотенциально имеет меньше ограничений. Для коллекций java.util, Iterable.forEachкак правило , будут использоваться эти коллекции Iterator, большинство из которых предназначены для быстрого отказа и которые будут выбрасываться, ConcurrentModificationExceptionесли коллекция будет структурно изменена во время итерации. Однако модификации, которые не являются структурными , допускаются во время итерации. Например, документация класса ArrayList гласит: «Простая установка значения элемента не является структурной модификацией». Таким образом, действие дляArrayList.forEachразрешено устанавливать значения в базовом ArrayListбез проблем.

Параллельные коллекции еще раз отличаются. Вместо быстрого отказа они разработаны как слабо последовательные . Полное определение по этой ссылке. Кратко, однако, рассмотрим ConcurrentLinkedDeque. Действие, переданное его forEachметоду, может изменять базовую деку, даже структурно, и ConcurrentModificationExceptionникогда не генерируется. Тем не менее, изменения, которые происходят, могут или не могут быть видны в этой итерации. (Отсюда и «слабая» последовательность.)

Еще одно отличие видно, если Iterable.forEachперебирает синхронизированную коллекцию. В такой коллекции один раз Iterable.forEach принимает блокировку коллекции и удерживает ее во всех вызовах метода действия. Stream.forEachВызов использует spliterator своей коллекции, которая не замок, и которая опирается на преобладающих правило невмешательства. Коллекция, поддерживающая поток, может быть изменена во время итерации, и, если это так, это ConcurrentModificationExceptionможет привести к несовместимому поведению.

Стюарт Маркс
источник
Iterable.forEach takes the collection's lock, Откуда эта информация? Я не могу найти такое поведение в источниках JDK.
turbanoff
9
См. Hg.openjdk.java.net/jdk8/jdk8/jdk/file/jdk8-b132/src/share/… например.
Стюарт Маркс
@Stuart, можешь рассказать о невмешательстве. Stream.forEach () также вызовет исключение ConcurrentModificationException (по крайней мере, для меня).
Юранос
1
@ yuranos87 Многие коллекции, такие как, ArrayListимеют довольно строгую проверку на одновременную модификацию и, следовательно, часто выбрасывают ConcurrentModificationException. Но это не гарантировано, особенно для параллельных потоков. Вместо CME вы можете получить неожиданный ответ. Рассмотрим также неструктурные модификации источника потока. Для параллельных потоков вы не знаете, какой поток будет обрабатывать конкретный элемент, и не был ли он обработан во время его изменения. Это устанавливает условие гонки, при котором вы можете получать разные результаты при каждом заезде и никогда не получать CME.
Стюарт Маркс
31

Этот ответ касается производительности различных реализаций циклов. Это только незначительно актуально для циклов, которые называются ОЧЕНЬ ЧАСТО (как миллионы вызовов). В большинстве случаев содержимое цикла будет самым дорогим элементом. Для ситуаций, когда вы делаете петли действительно часто, это все еще может представлять интерес.

Вы должны повторить эти тесты в целевой системе, поскольку это зависит от конкретной реализации ( полный исходный код ).

Я запускаю openjdk версии 1.8.0_111 на быстрой машине с Linux.

Я написал тест, который зацикливается на 10 ^ 6 раз по списку, используя этот код с различными размерами для integers(10 ^ 0 -> 10 ^ 5 записей).

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

Но все же в худших ситуациях циклическое выполнение 10 ^ 5 записей 10 ^ 6 раз занимало 100 секунд для худшего, поэтому другие соображения важнее практически во всех ситуациях.

public int outside = 0;

private void forCounter(List<Integer> integers) {
    for(int ii = 0; ii < integers.size(); ii++) {
        Integer next = integers.get(ii);
        outside = next*next;
    }
}

private void forEach(List<Integer> integers) {
    for(Integer next : integers) {
        outside = next * next;
    }
}

private void iteratorForEach(List<Integer> integers) {
    integers.forEach((ii) -> {
        outside = ii*ii;
    });
}
private void iteratorStream(List<Integer> integers) {
    integers.stream().forEach((ii) -> {
        outside = ii*ii;
    });
}

Вот мои сроки: миллисекунды / функция / количество записей в списке. Каждый прогон составляет 10 ^ 6 петель.

                           1    10    100    1000    10000
       iterator.forEach   27   116    959    8832    88958
               for:each   53   171   1262   11164   111005
         for with index   39   112    920    8577    89212
iterable.stream.forEach  255   324   1030    8519    88419

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


Использование MacBook Pro, 2,5 ГГц Intel Core i7, 16 ГБ, macOS 10.12.6:

                           1    10    100    1000    10000
       iterator.forEach   27   106   1047    8516    88044
               for:each   46   143   1182   10548   101925
         for with index   49   145    887    7614    81130
iterable.stream.forEach  393   397   1108    8908    88361

Java 8 Hotspot VM - 3,4 ГГц Intel Xeon, 8 ГБ, Windows 10 Pro

                            1    10    100    1000    10000
        iterator.forEach   30   115    928    8384    85911
                for:each   40   125   1166   10804   108006
          for with index   30   120    956    8247    81116
 iterable.stream.forEach  260   237   1020    8401    84883

Java 11 Hotspot VM - 3,4 ГГц Intel Xeon, 8 ГБ, Windows 10 Pro
(тот же компьютер, что и выше, другая версия JDK)

                            1    10    100    1000    10000
        iterator.forEach   20   104    940    8350    88918
                for:each   50   140    991    8497    89873
          for with index   37   140    945    8646    90402
 iterable.stream.forEach  200   270   1054    8558    87449

Java 11 OpenJ9 VM - 3,4 ГГц Intel Xeon, 8 ГБ, Windows 10 Pro
(та же машина и версия JDK, что и выше, разные виртуальные машины)

                            1    10    100    1000    10000
        iterator.forEach  211   475   3499   33631   336108
                for:each  200   375   2793   27249   272590
          for with index  384   467   2718   26036   261408
 iterable.stream.forEach  515   714   3096   26320   262786

Java 8 Hotspot VM - 2,8 ГГц AMD, 64 ГБ, Windows Server 2016

                            1    10    100    1000    10000
        iterator.forEach   95   192   2076   19269   198519
                for:each  157   224   2492   25466   248494
          for with index  140   368   2084   22294   207092
 iterable.stream.forEach  946   687   2206   21697   238457

Java 11 Hotspot VM - 2,8 ГГц AMD, 64 ГБ, Windows Server 2016
(тот же компьютер, что и выше, другая версия JDK)

                            1    10    100    1000    10000
        iterator.forEach   72   269   1972   23157   229445
                for:each  192   376   2114   24389   233544
          for with index  165   424   2123   20853   220356
 iterable.stream.forEach  921   660   2194   23840   204817

Java 11 OpenJ9 VM - 2,8 ГГц AMD, 64 ГБ, Windows Server 2016
(та же машина и версия JDK, что и выше, другая виртуальная машина)

                            1    10    100    1000    10000
        iterator.forEach  592   914   7232   59062   529497
                for:each  477  1576  14706  129724  1190001
          for with index  893   838   7265   74045   842927
 iterable.stream.forEach 1359  1782  11869  104427   958584

Реализация виртуальной машины, которую вы выбираете, также имеет значение Hotspot / OpenJ9 / etc.

Анджело Фукс
источник
3
Это очень хороший ответ, спасибо! Но с первого взгляда (а также со второго) неясно, какой метод соответствует какому эксперименту.
Торина
Я чувствую, что этот ответ требует больше голосов за тест кода :).
Кори
для
Centos
8

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

Внутренне stream()версия имеет несколько больше накладных расходов из-за создания объекта, но, смотря на время выполнения, она тоже не имеет никаких накладных расходов.

Обе реализации заканчивают тем, что перебирают collectionсодержимое один раз, и во время итерации распечатывают элемент.

skiwi
источник
Затраты на создание объекта, о которых вы упомянули, вы ссылаетесь на Streamсоздаваемый объект или отдельные объекты? AFAIK, Streamа не дублирует элементы.
Раффи Хачадоуриан
30
Этот ответ, кажется, противоречит превосходному ответу, написанному джентльменом, который разрабатывает базовые библиотеки Java в Oracle Corporation.
Дауд ибн Карим
0

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

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