Was ist der Unterschied zwischen Collection.stream (). ForEach () und Collection.forEach ()?


286

Ich verstehe, dass .stream()ich mit Kettenoperationen wie .filter()oder parallelen Stream verwenden kann. Aber was ist der Unterschied zwischen ihnen, wenn ich kleine Operationen ausführen muss (zum Beispiel das Drucken der Elemente der Liste)?

collection.stream().forEach(System.out::println);
collection.forEach(System.out::println);

Antworten:


287

Für einfache Fälle wie den abgebildeten sind sie meistens gleich. Es gibt jedoch eine Reihe subtiler Unterschiede, die erheblich sein können.

Ein Problem ist die Bestellung. Mit Stream.forEachist die Reihenfolge undefiniert . Es ist unwahrscheinlich, dass es bei sequentiellen Streams auftritt, dennoch liegt es innerhalb der Spezifikation für Stream.forEachdie Ausführung in einer beliebigen Reihenfolge. Dies tritt häufig in parallelen Streams auf. Im Gegensatz dazu Iterable.forEachwird immer in der Iterationsreihenfolge von ausgeführt Iterable, wenn eine angegeben ist.

Ein weiteres Problem sind Nebenwirkungen. Die angegebene Aktion in Stream.forEacherforderlich sein , nicht störenden . (Siehe das Dokument java.util.stream-Paket .) Iterable.forEachMöglicherweise gibt es weniger Einschränkungen. Bei den Sammlungen im java.util, Iterable.forEachwird in der Regel , dass die Sammlung verwenden Iterator, von denen die meisten sind so konzipiert , um ausfall schnell und die werfen , ConcurrentModificationExceptionwenn die Sammlung strukturell während der Iteration modifiziert wird. Änderungen, die nicht strukturell sind , sind jedoch während der Iteration zulässig. In der Dokumentation zur ArrayList-Klasse heißt es beispielsweise : "Das bloße Festlegen des Werts eines Elements ist keine strukturelle Änderung." Somit ist die Aktion fürArrayList.forEachdarf ArrayListproblemlos Werte im Basiswert setzen .

Die gleichzeitigen Sammlungen sind wieder anders. Anstatt ausfallsicher zu sein, sind sie so konzipiert, dass sie schwach konsistent sind . Die vollständige Definition finden Sie unter diesem Link. Überlegen Sie jedoch kurz ConcurrentLinkedDeque. Die Aktion , die seine übergeben forEachMethode ist erlaubt das darunter liegende deque zu ändern, auch strukturell und ConcurrentModificationExceptionwird nie geworfen. Die vorgenommene Änderung kann jedoch in dieser Iteration sichtbar sein oder nicht. (Daher die "schwache" Konsistenz.)

Ein weiterer Unterschied ist sichtbar, wenn Iterable.forEacheine synchronisierte Sammlung durchlaufen wird. Nimmt bei einer solchen Iterable.forEach Sammlung die Sperre der Sammlung einmal und hält sie für alle Aufrufe der Aktionsmethode. Der Stream.forEachAufruf verwendet den Spliterator der Sammlung, der nicht sperrt und auf der geltenden Regel der Nichteinmischung beruht. Die Sammlung, die den Stream unterstützt, kann während der Iteration geändert werden. Andernfalls kann es zu einem ConcurrentModificationExceptionoder inkonsistenten Verhalten kommen.


Iterable.forEach takes the collection's lock. Woher kommen diese Informationen? Ich kann ein solches Verhalten in JDK-Quellen nicht finden.
Turbanoff


@Stuart, können Sie näher darauf eingehen, nicht zu stören. Stream.forEach () löst auch ConcurrentModificationException aus (zumindest für mich).
Yuranos

1
@ yuranos87 Viele Sammlungen, wie z. B. ArrayListeine ziemlich strenge Überprüfung auf gleichzeitige Änderungen, werden daher häufig geworfen ConcurrentModificationException. Dies ist jedoch nicht garantiert, insbesondere für parallele Streams. Anstelle von CME erhalten Sie möglicherweise eine unerwartete Antwort. Berücksichtigen Sie auch nicht strukturelle Änderungen an der Stream-Quelle. Bei parallelen Streams wissen Sie nicht, welcher Thread ein bestimmtes Element verarbeitet oder ob es zum Zeitpunkt der Änderung verarbeitet wurde. Dadurch wird eine Rennbedingung festgelegt, bei der Sie bei jedem Lauf unterschiedliche Ergebnisse erzielen und niemals ein CME erhalten.
Stuart Marks

30

Diese Antwort befasst sich mit der Leistung der verschiedenen Implementierungen der Schleifen. Es ist nur unwesentlich relevant für Schleifen, die SEHR OFT genannt werden (wie Millionen von Anrufen). In den meisten Fällen ist der Inhalt der Schleife bei weitem das teuerste Element. In Situationen, in denen Sie sehr oft eine Schleife ausführen, ist dies möglicherweise immer noch von Interesse.

Sie sollten diese Tests unter dem Zielsystem wiederholen, da dies implementierungsspezifisch ist ( vollständiger Quellcode ).

Ich führe openjdk Version 1.8.0_111 auf einem schnellen Linux-Computer aus.

Ich habe einen Test geschrieben, der 10 ^ 6 Mal eine Liste durchläuft, wobei dieser Code mit unterschiedlichen Größen für integers(10 ^ 0 -> 10 ^ 5 Einträge) verwendet wird.

Die Ergebnisse sind unten aufgeführt. Die schnellste Methode hängt von der Anzahl der Einträge in der Liste ab.

Aber immer noch in den schlimmsten Situationen dauerte das Schleifen über 10 ^ 5 Einträge 10 ^ 6 Mal 100 Sekunden für den schlechtesten Darsteller, sodass andere Überlegungen in praktisch allen Situationen wichtiger sind.

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

Hier sind meine Timings: Millisekunden / Funktion / Anzahl der Einträge in der Liste. Jeder Lauf besteht aus 10 ^ 6 Schleifen.

                           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

Wenn Sie das Experiment wiederholen, habe ich den vollständigen Quellcode veröffentlicht . Bitte bearbeiten Sie diese Antwort und fügen Sie Ihre Ergebnisse mit einer Notation des getesteten Systems hinzu.


Verwenden eines MacBook 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
(gleicher Computer wie oben, andere JDK-Version)

                            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
(gleicher Computer und JDK-Version wie oben, unterschiedliche 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

Java 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
(gleicher Computer wie oben, andere JDK-Version)

                            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
(gleiche Maschine und JDK-Version wie oben, unterschiedliche 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

Die von Ihnen ausgewählte VM-Implementierung macht auch einen Unterschied. Hotspot / OpenJ9 / etc.


3
Das ist eine sehr schöne Antwort, danke! Aber auf den ersten Blick (und auch auf den zweiten) ist unklar, welche Methode welchem ​​Experiment entspricht.
Torina

Ich denke, diese Antwort braucht mehr Stimmen für den Code-Test :).
Cory


8

Es gibt keinen Unterschied zwischen den beiden, die Sie erwähnt haben, zumindest konzeptionell, das Collection.forEach()ist nur eine Abkürzung.

Intern hat die stream()Version aufgrund der Objekterstellung etwas mehr Overhead, aber in Bezug auf die Laufzeit hat sie dort auch keinen Overhead.

Beide Implementierungen iterieren einmal über den collectionInhalt und drucken während der Iteration das Element aus.


Beziehen Sie sich bei dem von Ihnen erwähnten Aufwand für die Objekterstellung auf das zu erstellende Objekt Streamoder auf die einzelnen Objekte? AFAIK, a Streamdupliziert die Elemente nicht.
Raffi Khatchadourian

30
Diese Antwort scheint der hervorragenden Antwort des Herrn zu widersprechen, der Java-Kernbibliotheken bei der Oracle Corporation entwickelt.
Dawood ibn Kareem

0

Collection.forEach () verwendet den Iterator der Sammlung (falls einer angegeben ist). Das heißt, die Verarbeitungsreihenfolge der Artikel ist definiert. Im Gegensatz dazu ist die Verarbeitungsreihenfolge von Collection.stream (). ForEach () undefiniert.

In den meisten Fällen spielt es keine Rolle, welche der beiden wir wählen. Parallele Streams ermöglichen es uns, den Stream in mehreren Threads auszuführen, und in solchen Situationen ist die Ausführungsreihenfolge undefiniert. In Java müssen nur alle Threads beendet sein, bevor eine Terminaloperation wie Collectors.toList () aufgerufen wird. Schauen wir uns ein Beispiel an, in dem wir erstens forEach () direkt in der Sammlung und zweitens in einem parallelen Stream aufrufen:

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

Wenn wir den Code mehrmals ausführen, sehen wir, dass list.forEach () die Elemente in Einfügereihenfolge verarbeitet, während list.parallelStream (). ForEach () bei jedem Lauf ein anderes Ergebnis erzeugt. Eine mögliche Ausgabe ist:

ABCD CDBA

Ein anderer ist:

ABCD DBCA
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.