Der Unterschied zwischen Collection.stream (). ForEach () und Collection.forEach ()

1. Einleitung

Es gibt verschiedene Optionen, um eine Sammlung in Java zu durchlaufen. In diesem kurzen Tutorial werden zwei ähnlich aussehende Ansätze vorgestellt - Collection.stream (). ForEach () und Collection.forEach () .

In den meisten Fällen führen beide zu den gleichen Ergebnissen, es gibt jedoch einige subtile Unterschiede, die wir betrachten werden.

2. Übersicht

Lassen Sie uns zunächst eine Liste erstellen, über die Sie iterieren möchten:

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

Am einfachsten ist es, die erweiterte for-Schleife zu verwenden:

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

Wenn wir Java im funktionalen Stil verwenden möchten, können wir auch forEach () verwenden . Wir können dies direkt in der Sammlung tun:

Consumer consumer = s -> { System.out::println }; list.forEach(consumer); 

Oder wir können forEach () im Stream der Sammlung aufrufen :

list.stream().forEach(consumer); 

Beide Versionen durchlaufen die Liste und drucken alle Elemente:

ABCD ABCD

In diesem einfachen Fall macht es keinen Unterschied, welche forEach () wir verwenden.

3. Ausführungsreihenfolge

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.

3.1. Parallele Streams

Parallele Streams ermöglichen es uns, den Stream in mehreren Threads auszuführen, und in solchen Situationen ist die Ausführungsreihenfolge undefiniert. Java erfordert nur, dass alle Threads beendet werden, 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

3.2. Benutzerdefinierte Iteratoren

Definieren wir eine Liste mit einem benutzerdefinierten Iterator, um die Sammlung in umgekehrter Reihenfolge zu durchlaufen:

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

Wenn wir die Liste durchlaufen, erneut mit forEach () direkt in der Sammlung und dann im Stream:

List myList = new ReverseList(); myList.addAll(list); myList.forEach(System.out::print); System.out.print(" "); myList.stream().forEach(System.out::print); 

Wir erhalten unterschiedliche Ergebnisse:

DCBA ABCD 

Der Grund für die unterschiedlichen Ergebnisse ist, dass forEach (), das direkt in der Liste verwendet wird, den benutzerdefinierten Iterator verwendet, während stream (). ForEach () einfach Elemente einzeln aus der Liste nimmt und den Iterator ignoriert.

4. Änderung der Sammlung

Viele Sammlungen (z. B. ArrayList oder HashSet ) sollten beim Durchlaufen nicht strukturell geändert werden. Wenn ein Element während einer Iteration entfernt oder hinzugefügt wird, wird eine ConcurrentModification- Ausnahme angezeigt .

Darüber hinaus sind Sammlungen so konzipiert, dass sie schnell ausfallen. Dies bedeutet, dass die Ausnahme ausgelöst wird, sobald Änderungen vorgenommen werden.

Ebenso erhalten wir eine ConcurrentModification- Ausnahme, wenn wir während der Ausführung der Stream-Pipeline ein Element hinzufügen oder entfernen. Die Ausnahme wird jedoch später ausgelöst.

Ein weiterer subtiler Unterschied zwischen den beiden forEach () -Methoden besteht darin, dass Java das explizite Ändern von Elementen mithilfe des Iterators zulässt. Im Gegensatz dazu sollten Streams nicht störend sein.

Schauen wir uns das Entfernen und Ändern von Elementen genauer an.

4.1. Ein Element entfernen

Definieren wir eine Operation, die das letzte Element ("D") unserer Liste entfernt:

Consumer removeElement = s -> { System.out.println(s + " " + list.size()); if (s != null && s.equals("A")) { list.remove("D"); } };

Wenn wir die Liste durchlaufen, wird das letzte Element entfernt, nachdem das erste Element („A“) gedruckt wurde:

list.forEach(removeElement);

Da forEach () ausfallsicher ist, beenden wir die Iteration und sehen eine Ausnahme, bevor das nächste Element verarbeitet wird:

A 4 Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList.forEach(ArrayList.java:1252) at ReverseList.main(ReverseList.java:1)

Let's see what happens if we use stream().forEach() instead:

list.stream().forEach(removeElement);

Here, we continue iterating over the whole list before we see an exception:

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)

However, Java does not guarantee that a ConcurrentModificationException is thrown at all. That means we should never write a program that depends on this exception.

4.2. Changing Elements

We can change an element while iterating over a list:

list.forEach(e -> { list.set(3, "E"); });

However, while there is no problem with doing this using either Collection.forEach() or stream().forEach(), Java requires an operation on a stream to be non-interfering. This means that elements shouldn't be modified during the execution of the stream pipeline.

The reason behind this is that the stream should facilitate parallel execution. Here, modifying elements of a stream could lead to unexpected behavior.

5. Conclusion

In this article, we saw some examples that show the subtle differences between Collection.forEach() and Collection.stream().forEach().

However, it's important to note that all the examples shown above are trivial and are merely meant to compare the two ways of iterating over a collection. We shouldn't write code whose correctness relies on the shown behavior.

If we don't require a stream but only want to iterate over a collection, the first choice should be using forEach() directly on the collection.

Der Quellcode für die Beispiele in diesem Artikel ist auf GitHub verfügbar.