Jaka jest różnica między Collection.stream (). ForEach () a Collection.forEach ()?

286

Rozumiem, że za pomocą .stream()mogę korzystać z operacji łańcuchowych takich jak .filter()strumień równoległy lub z niego korzystać. Ale jaka jest różnica między nimi, jeśli muszę wykonać małe operacje (na przykład wydrukować elementy listy)?

collection.stream().forEach(System.out::println);
collection.forEach(System.out::println);
VladS
źródło

Odpowiedzi:

287

W przypadku prostych przypadków, takich jak pokazany, są one w większości takie same. Istnieje jednak wiele subtelnych różnic, które mogą być znaczące.

Jeden problem dotyczy zamawiania. Z Stream.forEach, kolejność jest niezdefiniowana . Jest mało prawdopodobne, aby wystąpiło to z sekwencyjnymi strumieniami, ale jest to zgodne ze specyfikacją Stream.forEachdo wykonywania w dowolnej kolejności. Zdarza się to często w równoległych strumieniach. Natomiast Iterable.forEachjest zawsze wykonywane w kolejności iteracji Iterable, jeśli jest określony.

Innym problemem są skutki uboczne. Działanie określone w Stream.forEachmusi być niezakłócające . (Zobacz dokumentację pakietu java.util.stream .) Iterable.forEachPotencjalnie ma mniej ograniczeń. W przypadku kolekcji w java.util, na Iterable.forEachogół używa tej kolekcji Iterator, z których większość jest zaprojektowana tak, aby działała szybko i niezawodnie, i która wyrzuci, ConcurrentModificationExceptionjeśli kolekcja zostanie zmodyfikowana strukturalnie podczas iteracji. Jednak modyfikacje, które nie strukturalne, dozwolone podczas iteracji. Na przykład dokumentacja klasy ArrayList mówi „zwykłe ustawienie wartości elementu nie jest modyfikacją strukturalną”. Tak więc akcja dlaArrayList.forEachwolno ArrayListbez problemu ustawiać wartości w instrumencie bazowym .

Współbieżne kolekcje są jeszcze inne. Zamiast szybko działać, są zaprojektowane tak, aby były słabo spójne . Pełna definicja znajduje się pod tym linkiem. Zastanów się jednak krótko ConcurrentLinkedDeque. Przekazane jej działanie forEachmetody jest możliwość modyfikowania podstawowej deque, nawet strukturalnie, a ConcurrentModificationExceptionnigdy nie jest wyrzucane. Jednak wprowadzona modyfikacja może, ale nie musi być widoczna w tej iteracji. (Stąd „słaba” konsystencja).

Jeszcze inna różnica jest widoczna, jeśli Iterable.forEachiteruje zsynchronizowaną kolekcję. W takiej kolekcji Iterable.forEach wystarczy raz zablokować kolekcję i przytrzymać ją we wszystkich wywołaniach metody akcji. W Stream.forEachwywołaniu zastosowano spliterator kolekcji, który nie blokuje się i który opiera się na dominującej zasadzie nieingerencji. Kolekcja wspierająca strumień może być modyfikowana podczas iteracji, a jeśli tak, ConcurrentModificationExceptionmoże to spowodować niespójne zachowanie.

Znaki Stuarta
źródło
Iterable.forEach takes the collection's lock. Skąd ta informacja? Nie mogę znaleźć takiego zachowania w źródłach JDK.
turbanoff,
9
Zobacz na przykład hg.openjdk.java.net/jdk8/jdk8/jdk/file/jdk8-b132/src/share/ ...
Stuart Marks
@Stuart, czy możesz rozwinąć temat nieingerencji. Stream.forEach () wyrzuci także ConcurrentModificationException (przynajmniej dla mnie).
yuranos
1
@ yuranos87 Wiele kolekcji, takich jak ArrayListdość rygorystyczne sprawdzanie pod kątem równoczesnej modyfikacji, i dlatego często będą rzucać ConcurrentModificationException. Ale nie jest to gwarantowane, szczególnie w przypadku strumieni równoległych. Zamiast CME możesz otrzymać nieoczekiwaną odpowiedź. Rozważ także modyfikacje niestrukturalne źródła strumienia. W przypadku strumieni równoległych nie wiadomo, który wątek przetworzy dany element, ani czy został on przetworzony w momencie jego modyfikacji. To ustanawia warunki wyścigu, w których możesz uzyskać różne wyniki przy każdym biegu i nigdy nie uzyskać CME.
Stuart Marks
30

Ta odpowiedź dotyczy samego wykonania różnych implementacji pętli. Jest to tylko nieznacznie istotne dla pętli, które są nazywane BARDZO CZĘSTO (jak miliony połączeń). W większości przypadków zawartość pętli będzie zdecydowanie najdroższym elementem. W sytuacjach, w których często zapętlasz się, może to nadal być interesujące.

Powtórz te testy w systemie docelowym, ponieważ jest to specyficzne dla implementacji ( pełny kod źródłowy ).

Używam openjdk w wersji 1.8.0_111 na szybkim komputerze z systemem Linux.

Napisałem test, który zapętla 10 ^ 6 razy nad Listą, używając tego kodu o różnych rozmiarach dla integers(10 ^ 0 -> 10 ^ 5 wpisów).

Wyniki są poniżej, najszybsza metoda różni się w zależności od liczby wpisów na liście.

Ale wciąż w najgorszych sytuacjach zapętlenie ponad 10 ^ 5 wpisów 10 ^ 6 razy zajęło 100 sekund dla najgorzej wykonującego, więc inne względy są ważniejsze w praktycznie wszystkich sytuacjach.

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;
    });
}

Oto moje czasy: milisekundy / funkcja / liczba pozycji na liście. Każdy bieg to 10 ^ 6 pętli.

                           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

Jeśli powtórzysz eksperyment, opublikowałem pełny kod źródłowy . Przeprowadź edycję tej odpowiedzi i dodaj wyniki z notacją testowanego systemu.


Używając MacBooka Pro, 2,5 GHz Intel Core i7, 16 GB, 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 GHz Intel Xeon, 8 GB, 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 GHz Intel Xeon, 8 GB, Windows 10 Pro
(ta sama maszyna jak powyżej, inna wersja 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 GHz Intel Xeon, 8 GB, Windows 10 Pro
(ta sama maszyna i wersja JDK jak wyżej, inna VM)

                            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

VM 8 Hotspot VM - 2,8 GHz AMD, 64 GB, 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 GHz AMD, 64 GB, Windows Server 2016
(ta sama maszyna jak powyżej, inna wersja 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 GHz AMD, 64 GB, Windows Server 2016
(ta sama maszyna i wersja JDK jak wyżej, inna VM)

                            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

Wybrana implementacja maszyny wirtualnej ma również znaczenie Hotspot / OpenJ9 / etc.

Angelo Fuchs
źródło
3
To bardzo miła odpowiedź, dzięki! Ale od pierwszego spojrzenia (a także od drugiego) nie jest jasne, która metoda odpowiada eksperymentowi.
torina
Wydaje mi się, że ta odpowiedź wymaga więcej głosów na test kodu :).
Cory
dla przykładów testów +1
Centos
8

Nie ma różnicy między tymi dwoma, o których wspomniałeś, przynajmniej koncepcyjnie, Collection.forEach()jest to tylko skrót.

Wewnętrznie stream()wersja ma nieco więcej narzutu z powodu tworzenia obiektów, ale patrząc na czas działania, nie ma tam też narzutu.

Obie implementacje kończą się collectionraz iteracją zawartości, a podczas iteracji wypisują element.

skiwi
źródło
Narzut związany z tworzeniem obiektów, o którym wspominasz, czy odnosisz się do Streamtworzonego obiektu czy poszczególnych obiektów? AFAIK, a Streamnie powiela elementów.
Raffi Khatchadourian
30
Ta odpowiedź wydaje się zaprzeczać doskonałej odpowiedzi napisanej przez dżentelmena, który opracowuje podstawowe biblioteki Java w Oracle Corporation.
Dawood ibn Kareem,
0

Collection.forEach () używa iteratora kolekcji (jeśli jest określony). Oznacza to, że kolejność przetwarzania pozycji jest zdefiniowana. Natomiast kolejność przetwarzania Collection.stream (). ForEach () jest niezdefiniowana.

W większości przypadków nie ma znaczenia, który z dwóch wybieramy. Równoległe strumienie pozwalają nam na wykonanie strumienia w wielu wątkach, aw takich sytuacjach kolejność wykonywania jest niezdefiniowana. Java wymaga jedynie zakończenia wszystkich wątków przed wywołaniem jakiejkolwiek operacji terminalowej, takiej jak Collectors.toList (). Spójrzmy na przykład, w którym najpierw wywołujemy forEach () bezpośrednio w kolekcji, a po drugie w równoległym strumieniu:

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

Jeśli uruchomimy kod kilka razy, zobaczymy, że list.forEach () przetwarza elementy w kolejności wstawiania, podczas gdy list.parallelStream (). ForEach () generuje inny wynik przy każdym uruchomieniu. Jednym z możliwych wyników jest:

ABCD CDBA

Kolejny to:

ABCD DBCA
cpatel
źródło