J'ai une liste myListToParse
où je veux filtrer les éléments et appliquer une méthode à chaque élément, puis ajouter le résultat dans une autre liste myFinalList
.
Avec Java 8, j'ai remarqué que je pouvais le faire de 2 manières différentes. J'aimerais connaître le moyen le plus efficace entre eux et comprendre pourquoi un moyen est meilleur 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
il ne nécessite pas de muter une collection qui existe en dehors de l'expression lambda,
il est plus lisible car les différentes étapes effectuées dans le pipeline de collecte sont écrites séquentiellement (une opération de filtrage, puis une opération de carte, puis la collecte du résultat), Fowler's excellent article )
vous pouvez facilement modifier la manière dont les valeurs sont collectées en remplaçant la variable Collector
utilisée. Dans certains cas, vous aurez peut-être besoin d'écrire votre propre Collector
, mais l'avantage est que vous pouvez facilement le réutiliser.
Je suis d’accord avec les réponses existantes pour dire que la seconde forme est préférable car elle n’a aucun effet secondaire et est plus facile à mettre en parallèle (utilisez simplement un flux parallèle).
En termes de performances, il semble qu'elles soient équivalentes jusqu'à ce que vous commenciez à utiliser des flux parallèles. Dans ce cas, map fonctionnera vraiment beaucoup mieux. Voir ci-dessous le micro benchmark résultats:
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 amplifier le premier exemple de la même manière, car forEach est une méthode de type terminal (elle retourne void), ce qui vous oblige à utiliser un état lambda. Mais c'est vraiment une mauvaise idée si vous utilisez des flux parallèles .
Notez enfin que votre deuxième extrait peut être écrit de manière légèrement plus concise avec des références de méthodes et des importations statiques:
myFinalList = myListToParse.stream()
.filter(Objects::nonNull)
.map(this::doSomething)
.collect(toList());
L'un des principaux avantages de l'utilisation des flux réside dans le fait qu'ils permettent 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-thread pour libre ce qui signifie qu'il n'est pas nécessaire d'écrire un code multi-thread supplémentaire pour rendre votre flux concurrent.
En supposant que la raison pour laquelle vous explorez ce style de programmation soit que vous souhaitiez exploiter ces avantages, votre premier exemple de code est potentiellement non fonctionnel car la méthode foreach
est classée comme étant terminale (ce qui signifie qu'elle peut produire des effets secondaires).
La deuxième façon est préférable du point de vue de la programmation fonctionnelle, car la fonction map peut accepter des fonctions lambda sans état. Plus explicitement, le lambda transmis à la fonction map devrait être
ArrayList
).Un autre avantage de la seconde approche est que si le flux est parallèle et que le collecteur est simultané et non ordonné, ces caractéristiques peuvent fournir des indications utiles sur l'opération de réduction permettant d'effectuer la collecte simultanément.
Si vous utilisez Collections Eclipse , vous pouvez utiliser la méthode collectIf()
.
MutableList<Integer> source =
Lists.mutable.with(1, null, 2, null, 3, null, 4, null, 5);
MutableList<String> result = source.collectIf(Objects::nonNull, String::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 flux.
Note: Je suis un partisan des collections Eclipse.
Je préfère la deuxième voie.
Lorsque vous utilisez la première méthode, si vous décidez d'utiliser un flux parallèle pour améliorer les performances, vous ne contrôlez pas l'ordre dans lequel les éléments seront ajoutés à la liste de sortie de forEach
.
Lorsque vous utilisez toList
, l'API Streams conserve l'ordre même si vous utilisez un flux parallèle.
Il existe une troisième option - en utilisant stream().toArray()
- voir les commentaires sous pourquoi le flux n'a pas de méthode toList . Il s'avère être plus lent que forEach () ou collect (), et moins expressif. Il pourrait être optimisé dans les versions ultérieures du JDK, ajoutez-le ici au cas où.
en supposant que List<String>
myFinalList = Arrays.asList(
myListToParse.stream()
.filter(Objects::nonNull)
.map(this::doSomething)
.toArray(String[]::new)
);
avec un micro-micro benchmark, 1M entrées, 20% de valeurs nuls et une simple transformation en DoWething ()
private LongSummaryStatistics benchmark(final String 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}
parallel without nulls and filter (le flux est donc SIZED
): toArrays présente les meilleures performances dans ce cas, et .forEach()
échoue avec "indexOutOfBounds" sur le destinataire ArrayList, devant être remplacé 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<Long> greaterThan100 = new Predicate<Long>() {
@Override
public boolean test(Long currentParameter) {
return currentParameter > 100;
}
};
List<Long> sourceLongList = Arrays.asList(1L, 10L, 50L, 80L, 100L, 120L, 133L, 333L);
List<Long> resultList = sourceLongList.parallelStream().filter(greaterThan100).collect(Collectors.toList());
Si utiliser 3rd Pary Libaries est ok cyclops-react définit les collections étendues Lazy avec cette fonctionnalité intégrée.
ListX myListToParse;
ListX myFinalList = myListToParse.filter (elt -> elt! = Null) .map (elt -> doQuelque chose (elt));
myFinalList n'est pas évalué avant le premier accès (et ensuite, la liste matérialisée est mise en cache et réutilisée).
[Divulgation Je suis le développeur principal de cyclops-react]