Java 8 – Meilleur moyen de transformer une liste: map ou foreach?

J’ai une liste myListToParse où je veux filtrer les éléments et appliquer une méthode sur chaque élément, et append le résultat dans une autre liste myFinalList .

Avec Java 8, j’ai remarqué que je pouvais le faire de deux manières différentes. Je voudrais connaître le moyen le plus efficace entre eux et comprendre pourquoi une voie est meilleure que l’autre.

Je suis ouvert à toute suggestion concernant une troisième voie.

Méthode 1:

 myFinalList = new ArrayList(); myListToParse.stream() .filter(elt -> elt != null) .forEach(elt -> myFinalList.add(doSomething(elt))); 

Méthode 2:

 myFinalList = myListToParse.stream() .filter(elt -> elt != null) .map(elt -> doSomething(elt)) .collect(Collectors.toList()); 

Ne vous inquiétez pas des différences de performances, elles seront minimes dans ce cas normalement.

La méthode 2 est préférable car

  1. il ne nécessite pas de muter une collection qui existe en dehors de l’expression lambda,

  2. c’est plus lisible car les différentes étapes du pipeline de collecte sont écrites séquentiellement (d’abord une opération de filtrage, puis une opération de carte, puis la collecte du résultat) (pour plus d’informations sur les avantages des pipelines de collecte, voir l’ excellent article de Martin Fowler ). )

  3. Vous pouvez facilement modifier le mode de collecte des valeurs en remplaçant le Collector utilisé. Dans certains cas, vous devrez peut-être écrire votre propre Collector , mais l’avantage est que vous pouvez facilement le réutiliser.

Je suis d’accord avec les réponses existantes selon lesquelles la seconde forme est meilleure car elle n’a aucun effet secondaire et est plus facile à paralléliser (utilisez simplement un stream parallèle).

Du sharepoint vue des performances, il semble qu’elles soient équivalentes jusqu’à ce que vous commenciez à utiliser des stream parallèles. Dans ce cas, la carte fonctionnera beaucoup mieux. Voir ci-dessous les résultats du micro-benchmark :

 Benchmark Mode Samples Score Error Units SO28319064.forEach avgt 100 187.310 ± 1.768 ms/op SO28319064.map avgt 100 189.180 ± 1.692 ms/op SO28319064.mapWithParallelStream avgt 100 55,577 ± 0,782 ms/op 

Vous ne pouvez pas augmenter le premier exemple de la même manière, car forEach est une méthode de terminal – elle renvoie un void – vous êtes donc obligé d’utiliser un lambda avec état. Mais c’est vraiment une mauvaise idée si vous utilisez des stream parallèles .

Enfin, notez que votre deuxième extrait peut être écrit de manière légèrement plus concise avec les références de méthode et les importations statiques:

 myFinalList = myListToParse.stream() .filter(Objects::nonNull) .map(this::doSomething) .collect(toList()); 

L’un des principaux avantages de l’utilisation des stream est qu’il permet de traiter des données de manière déclarative, c’est-à-dire en utilisant un style de programmation fonctionnel. Il offre également une capacité multi-threading, ce qui signifie qu’il n’est pas nécessaire d’écrire du code multithread supplémentaire pour rendre votre stream simultané.

En supposant que la raison pour laquelle vous explorez ce style de programmation est que vous souhaitez exploiter ces avantages, votre premier échantillon de code n’est potentiellement pas fonctionnel car la méthode foreach est considérée comme étant terminale (ce qui signifie qu’elle peut produire des effets secondaires).

La seconde manière est préférable du sharepoint vue de la functional programming puisque la fonction de carte peut accepter des fonctions lambda sans état. Plus explicitement, le lambda passé à la fonction map devrait être

  1. Non interférant, ce qui signifie que la fonction ne doit pas modifier la source du stream si elle n’est pas concurrente (par exemple, ArrayList ).
  2. Stateless pour éviter des résultats inattendus lors d’un parallel processing (provoqué par des différences de planification des threads).

Un autre avantage de la seconde approche est que si le stream est parallèle et que le collecteur est simultané et non ordonné, ces caractéristiques peuvent fournir des indications utiles à l’opération de réduction pour effectuer la collecte en même temps.

Si vous utilisez des collections Eclipse, vous pouvez utiliser la méthode collectIf() .

 MutableList source = Lists.mutable.with(1, null, 2, null, 3, null, 4, null, 5); MutableList result = source.collectIf(Objects::nonNull, Ssortingng::valueOf); Assert.assertEquals(Lists.immutable.with("1", "2", "3", "4", "5"), result); 

Il évalue avec impatience et devrait être un peu plus rapide que d’utiliser un stream.

Note: Je suis un committer pour les collections Eclipse.

Je préfère la seconde voie.

Lorsque vous utilisez la première méthode, si vous décidez d’utiliser un stream parallèle pour améliorer les performances, vous n’aurez aucun contrôle sur l’ordre dans lequel les éléments seront ajoutés à la liste de sortie par forEach .

Lorsque vous utilisez toList , l’API Streams conservera l’ordre même si vous utilisez un stream parallèle.

Il y a une troisième option – using stream().toArray() – voir les commentaires sous pourquoi ne pas diffuser une méthode toList . Il s’avère plus lent que forEach () ou collect () et moins expressif. Il pourrait être optimisé dans les versions ultérieures de JDK, l’ajoutant ici au cas où.

en supposant List

  myFinalList = Arrays.asList( myListToParse.stream() .filter(Objects::nonNull) .map(this::doSomething) .toArray(Ssortingng[]::new) ); 

avec un micro-benchmark, 1M d’entrées, 20% de nuls et une transformation simple dans doSomething ()

 private LongSummaryStatistics benchmark(final Ssortingng testName, final Runnable methodToTest, int samples) { long[] timing = new long[samples]; for (int i = 0; i < samples; i++) { long start = System.currentTimeMillis(); methodToTest.run(); timing[i] = System.currentTimeMillis() - start; } final LongSummaryStatistics stats = Arrays.stream(timing).summaryStatistics(); System.out.println(testName + ": " + stats); return stats; } 

les résultats sont

parallèle:

 toArray: LongSummaryStatistics{count=10, sum=3721, min=321, average=372,100000, max=535} forEach: LongSummaryStatistics{count=10, sum=3502, min=249, average=350,200000, max=389} collect: LongSummaryStatistics{count=10, sum=3325, min=265, average=332,500000, max=368} 

séquentiel:

 toArray: LongSummaryStatistics{count=10, sum=5493, min=517, average=549,300000, max=569} forEach: LongSummaryStatistics{count=10, sum=5316, min=427, average=531,600000, max=571} collect: LongSummaryStatistics{count=10, sum=5380, min=444, average=538,000000, max=557} 

parallèle sans null et filtre (le stream est donc SIZED ): toArrays a les meilleures performances dans ce cas, et .forEach() échoue avec "indexOutOfBounds" sur le composant ArrayList, a dû remplacer par .forEachOrdered()

 toArray: LongSummaryStatistics{count=100, sum=75566, min=707, average=755,660000, max=1107} forEach: LongSummaryStatistics{count=100, sum=115802, min=992, average=1158,020000, max=1254} collect: LongSummaryStatistics{count=100, sum=88415, min=732, average=884,150000, max=1014} 

Peut être la méthode 3.

Je préfère toujours garder la logique séparée.

 Predicate greaterThan100 = new Predicate() { @Override public boolean test(Long currentParameter) { return currentParameter > 100; } }; List sourceLongList = Arrays.asList(1L, 10L, 50L, 80L, 100L, 120L, 133L, 333L); List resultList = sourceLongList.parallelStream().filter(greaterThan100).collect(Collectors.toList()); 

Si utiliser 3e Pary Libaries est ok, cyclops- react définit les collections étendues paresseuses avec cette fonctionnalité intégrée. Par exemple, nous pourrions simplement écrire

ListX myListToParse;

ListX myFinalList = myListToParse.filter (elt -> elt! = Null) .map (elt -> doQuelque chose (elt));

myFinalList n’est pas évalué jusqu’au premier access (et là après que la liste matérialisée est mise en cache et réutilisée).

[Disclosure Je suis le développeur principal de cyclops-react]