Quelle est la différence entre Collection.stream (). ForEach () et Collection.forEach ()?


286

Je comprends qu'avec .stream(), je peux utiliser des opérations de chaîne comme .filter()ou utiliser un flux parallèle. Mais quelle est la différence entre eux si j'ai besoin d'exécuter de petites opérations (par exemple, imprimer les éléments de la liste)?

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

Réponses:


287

Pour les cas simples comme celui illustré, ils sont généralement les mêmes. Cependant, il existe un certain nombre de différences subtiles qui pourraient être importantes.

Un problème est lié à la commande. Avec Stream.forEach, la commande n'est pas définie . Il est peu probable que cela se produise avec des flux séquentiels, cependant, c'est dans la spécification pour Stream.forEachs'exécuter dans un ordre arbitraire. Cela se produit fréquemment dans les flux parallèles. En revanche, Iterable.forEachest toujours exécuté dans l'ordre d'itération du Iterable, si un est spécifié.

Un autre problème concerne les effets secondaires. L'action spécifiée dans Stream.forEachdoit être sans interférence . (Voir la documentation du package java.util.stream .) A Iterable.forEachpotentiellement moins de restrictions. Pour les collections dans java.util, Iterable.forEachutilisera généralement cette collection Iterator, dont la plupart sont conçues pour être rapides et qui jetteront ConcurrentModificationExceptionsi la collection est structurellement modifiée pendant l'itération. Cependant, les modifications qui ne sont pas structurelles sont autorisées pendant l'itération. Par exemple, la documentation de la classe ArrayList indique que "la simple définition de la valeur d'un élément n'est pas une modification structurelle". Ainsi, l'action pourArrayList.forEachest autorisé à définir des valeurs dans le sous-jacent ArrayListsans problème.

Les collections simultanées sont encore différentes. Au lieu d'être rapides, ils sont conçus pour être faiblement cohérents . La définition complète est à ce lien. En bref, cependant, réfléchissez ConcurrentLinkedDeque. L'action passée à sa forEachméthode est autorisée à modifier le deque sous-jacent, même structurellement, et ConcurrentModificationExceptionn'est jamais levée. Cependant, la modification qui se produit peut ou non être visible dans cette itération. (D'où la cohérence "faible".)

Une autre différence est encore visible si Iterable.forEachitère sur une collection synchronisée. Sur une telle collection, Iterable.forEach prend une fois le verrou de la collection et le maintient sur tous les appels à la méthode d'action. L' Stream.forEachappel utilise le séparateur de la collection, qui ne se verrouille pas et qui repose sur la règle de non-interférence en vigueur. La collection qui sauvegarde le flux peut être modifiée pendant l'itération, et si c'est le cas, un ConcurrentModificationExceptioncomportement incohérent ou peut en résulter.


Iterable.forEach takes the collection's lock. D'où proviennent ces informations? Je ne parviens pas à trouver un tel comportement dans les sources JDK.
turbanoff


@Stuart, pouvez-vous élaborer sur la non-interférence. Stream.forEach () lèvera également ConcurrentModificationException (au moins pour moi).
yuranos

1
@ yuranos87 De nombreuses collections comme celles qui ArrayListont une vérification assez stricte des modifications simultanées, sont donc souvent lancées ConcurrentModificationException. Mais cela n'est pas garanti, en particulier pour les flux parallèles. Au lieu de CME, vous pourriez obtenir une réponse inattendue. Pensez également aux modifications non structurelles de la source du flux. Pour les flux parallèles, vous ne savez pas quel thread traitera un élément particulier, ni s'il a été traité au moment de sa modification. Cela met en place une condition de concurrence, où vous pouvez obtenir des résultats différents à chaque course, et ne jamais obtenir un CME.
Stuart marque

30

Cette réponse concerne les performances des différentes implémentations des boucles. Ce n'est que marginalement pertinent pour les boucles qui sont appelées TRÈS SOUVENT (comme des millions d'appels). Dans la plupart des cas, le contenu de la boucle sera de loin l'élément le plus cher. Pour les situations où vous bouclez très souvent, cela peut toujours être intéressant.

Vous devez répéter ces tests sous le système cible car cela est spécifique à l'implémentation ( code source complet ).

J'exécute openjdk version 1.8.0_111 sur une machine Linux rapide.

J'ai écrit un test qui boucle 10 ^ 6 fois sur une liste en utilisant ce code avec différentes tailles pour integers(10 ^ 0 -> 10 ^ 5 entrées).

Les résultats sont ci-dessous, la méthode la plus rapide varie en fonction du nombre d'entrées dans la liste.

Mais toujours dans les pires situations, boucler plus de 10 ^ 5 entrées 10 ^ 6 fois a pris 100 secondes pour le moins performant, donc d'autres considérations sont plus importantes dans pratiquement toutes les situations.

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

Voici mes timings: millisecondes / fonction / nombre d'entrées dans la liste. Chaque exécution est de 10 ^ 6 boucles.

                           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

Si vous répétez l'expérience, j'ai publié le code source complet . Veuillez modifier cette réponse et ajouter vos résultats avec une notation du système testé.


À l'aide d'un MacBook Pro, Intel Core i7 2,5 GHz, 16 Go, 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 - Intel Xeon 3,4 GHz, 8 Go, 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 - Intel Xeon 3,4 GHz, 8 Go, Windows 10 Pro
(même machine que ci-dessus, version JDK différente)

                            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 - Intel Xeon 3,4 GHz, 8 Go, Windows 10 Pro
(même machine et version JDK que ci-dessus, VM différente)

                            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 Go, 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 Go, Windows Server 2016
(même machine que ci-dessus, version JDK différente)

                            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 Go, Windows Server 2016
(même machine et version JDK que ci-dessus, VM différente)

                            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

L'implémentation de VM que vous choisissez fait également une différence Hotspot / OpenJ9 / etc.


3
C'est une très belle réponse, merci! Mais à première vue (et aussi à partir du second), on ne sait pas quelle méthode correspond à quelle expérience.
torina

Je pense que cette réponse a besoin de plus de votes pour le test de code :).
Cory

pour des exemples de tests +1
Centos

8

Il n'y a aucune différence entre les deux que vous avez mentionnés, du moins sur le plan conceptuel, ce Collection.forEach()n'est qu'un raccourci.

En interne, la stream()version a un peu plus de surcharge en raison de la création d'objets, mais en regardant le temps d'exécution, elle n'a pas non plus de surcharge.

Les deux implémentations finissent par itérer collectionune fois sur le contenu et, pendant l'itération, impriment l'élément.


Les frais généraux de création d'objet que vous mentionnez, parlez-vous de la Streamcréation ou des objets individuels? AFAIK, a Streamne duplique pas les éléments.
Raffi Khatchadourian

30
Cette réponse semble contredire l'excellente réponse écrite par l'homme qui développe les bibliothèques de base Java chez Oracle Corporation.
Dawood ibn Kareem

0

Collection.forEach () utilise l'itérateur de la collection (s'il est spécifié). 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, cela ne fait aucune différence lequel des deux nous choisissons. 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 requiert uniquement la fin de tous les threads avant d'appeler toute opération de terminal, telle que Collectors.toList (). Regardons un exemple où nous appelons d'abord forEach () directement sur la collection, et deuxièmement, 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
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.