La différence entre Collection.stream (). ForEach () et Collection.forEach ()

1. Introduction

Il existe plusieurs options pour parcourir une collection en Java. Dans ce court didacticiel, nous examinerons deux approches similaires: Collection.stream (). ForEach () et Collection.forEach () .

Dans la plupart des cas, les deux donneront les mêmes résultats, cependant, nous allons examiner quelques différences subtiles.

2. Aperçu

Commençons par créer une liste à parcourir:

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

Le moyen le plus simple consiste à utiliser la boucle for améliorée:

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

Si nous voulons utiliser Java de style fonctionnel, nous pouvons également utiliser forEach () . Nous pouvons le faire directement sur la collection:

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

Ou, nous pouvons appeler forEach () sur le flux de la collection:

list.stream().forEach(consumer); 

Les deux versions vont parcourir la liste et imprimer tous les éléments:

ABCD ABCD

Dans ce cas simple, cela ne fait aucune différence avec forEach () que nous utilisons.

3. Ordre d'exécution

Collection.forEach () utilise l'itérateur de la collection (le cas échéant). Cela signifie que l'ordre de traitement des articles est défini. En revanche, l'ordre de traitement de Collection.stream (). ForEach () n'est pas défini.

Dans la plupart des cas, celui que nous choisissons ne fait aucune différence.

3.1. Flux parallèles

Les flux parallèles nous permettent d'exécuter le flux dans plusieurs threads, et dans de telles situations, l'ordre d'exécution n'est pas défini. Java nécessite uniquement que tous les threads se terminent avant qu'une opération de terminal, telle que Collectors.toList () , ne soit appelée.

Regardons un exemple où nous appelons d'abord forEach () directement sur la collection, et ensuite, sur un flux parallèle:

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

Si nous exécutons le code plusieurs fois, nous voyons que list.forEach () traite les éléments dans l'ordre d'insertion, tandis que list.parallelStream (). ForEach () produit un résultat différent à chaque exécution.

Une sortie possible est:

ABCD CDBA

Un autre est:

ABCD DBCA

3.2. Itérateurs personnalisés

Définissons une liste avec un itérateur personnalisé pour parcourir la collection dans l'ordre inverse:

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

Lorsque nous itérons sur la liste, à nouveau avec forEach () directement sur la collection puis sur le flux:

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

Nous obtenons des résultats différents:

DCBA ABCD 

La raison des résultats différents est que forEach () utilisé directement sur la liste utilise l'itérateur personnalisé, tandis que stream (). ForEach () prend simplement les éléments un par un de la liste, ignorant l'itérateur.

4. Modification de la collection

De nombreuses collections (par exemple, ArrayList ou HashSet ) ne devraient pas être modifiées structurellement lors de leur itération. Si un élément est supprimé ou ajouté au cours d'une itération, nous obtiendrons une exception ConcurrentModification .

De plus, les collections sont conçues pour échouer rapidement, ce qui signifie que l'exception est levée dès qu'il y a une modification.

De même, nous obtiendrons une exception ConcurrentModification lorsque nous ajoutons ou supprimons un élément lors de l'exécution du pipeline de flux. Cependant, l'exception sera levée plus tard.

Une autre différence subtile entre les deux méthodes forEach () est que Java permet explicitement de modifier des éléments à l'aide de l'itérateur. Les flux, en revanche, ne doivent pas interférer.

Examinons plus en détail la suppression et la modification d'éléments.

4.1. Suppression d'un élément

Définissons une opération qui supprime le dernier élément («D») de notre liste:

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

Lorsque nous parcourons la liste, le dernier élément est supprimé après l'impression du premier élément («A»):

list.forEach(removeElement);

Puisque forEach () est rapide, nous arrêtons l'itération et voyons une exception avant que l'élément suivant ne soit traité:

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.

Le code source des exemples de cet article est disponible à l'adresse over sur GitHub.