Java 8 - Meilleur moyen de transformer une liste: carte ou foreach?

188

J'ai une liste myListToParseoù je veux filtrer les éléments et appliquer une méthode sur chaque élément, et ajouter le résultat dans une autre liste myFinalList.

Avec Java 8, j'ai remarqué que je peux 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()); 
Emilien Brigand
la source
55
Le deuxième. Une fonction correcte ne devrait avoir aucun effet secondaire, dans votre première implémentation, vous modifiez le monde externe.
ThanksForAllTheFish
37
juste une question de style, mais elt -> elt != nullpeut être remplacé parObjects::nonNull
the8472
2
@ the8472 Encore mieux serait de s'assurer qu'il n'y a pas de valeurs nulles dans la collection en premier lieu, et de l'utiliser à la Optional<T>place en combinaison avec flatMap.
herman
2
@SzymonRoziewski, pas tout à fait. Pour quelque chose d'aussi trivial que cela, le travail nécessaire pour configurer le flux parallèle sous le capot rendra l'utilisation de cette construction muette.
MK
2
Notez que vous pouvez écrire en .map(this::doSomething)supposant qu'il doSomethings'agit d'une méthode non statique. S'il est statique, vous pouvez le remplacer thispar le nom de la classe.
herman le

Réponses:

153

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

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. il est plus lisible car les différentes étapes qui sont effectuées dans le 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 la façon dont les valeurs sont collectées en remplaçant le Collectorqui est utilisé. Dans certains cas, vous devrez peut-être écrire le vôtre Collector, mais l'avantage est que vous pouvez facilement le réutiliser.

Herman
la source
43

Je suis d'accord avec les réponses existantes que la deuxième forme est meilleure car elle n'a aucun effet secondaire et est plus facile à paralléliser (utilisez simplement un flux parallèle).

En termes de performances, il semble qu'ils soient équivalents jusqu'à ce que vous commenciez à utiliser des flux 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 booster le premier exemple de la même manière car forEach est une méthode de terminal - elle renvoie void - vous êtes donc obligé d'utiliser un lambda avec état. Mais c'est vraiment une mauvaise idée si vous utilisez des flux parallèles .

Notez enfin que votre deuxième extrait de code peut être écrit d'une manière légèrement plus concise avec des références de méthode et des importations statiques:

myFinalList = myListToParse.stream()
    .filter(Objects::nonNull)
    .map(this::doSomething)
    .collect(toList()); 
assylies
la source
1
Concernant les performances, dans votre cas, "map" l'emporte vraiment sur "forEach" si vous utilisez parallelStreams. Mes benchmaks en millisecondes: SO28319064.forEach: 187,310 ± 1,768 ms / op - SO28319064.map: 189,180 ± 1,692 ms / op --SO28319064.mapParallelStream: 55,577 ± 0,782 ms / op
Giuseppe Bertone
2
@GiuseppeBertone, c'est à assylias, mais à mon avis, votre modification contredit l'intention de l'auteur original. Si vous souhaitez ajouter votre propre réponse, il est préférable de l'ajouter au lieu de trop modifier l'existant. Désormais également, le lien vers le microbenchmark n'est plus pertinent pour les résultats.
Tagir Valeev
5

L'un des principaux avantages de l'utilisation de flux est qu'il permet de traiter les données de manière déclarative, c'est-à-dire en utilisant un style de programmation fonctionnel. Il offre également une capacité multi-threading gratuite, ce qui signifie qu'il n'est pas nécessaire d'écrire du code multi-thread supplémentaire pour rendre votre flux 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 foreachméthode est classée comme étant terminale (ce qui signifie qu'elle peut produire des effets secondaires).

La deuxième méthode est préférée du point de vue de la programmation fonctionnelle, car la fonction de carte peut accepter des fonctions lambda sans état. Plus explicitement, le lambda passé à la fonction map doit être

  1. Non interférant, ce qui signifie que la fonction ne doit pas modifier la source du flux si elle n'est pas concurrente (par exemple ArrayList).
  2. Stateless pour éviter des résultats inattendus lors d'un traitement parallèle (causé par des différences de planification de thread).

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 à l'opération de réduction pour effectuer la collecte simultanément.

MK
la source
4

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

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 empressement et devrait être un peu plus rapide que l'utilisation d'un Stream.

Remarque: je suis un committer pour les collections Eclipse.

Craig P. Motlin
la source
1

Je préfère la deuxième manière.

Lorsque vous utilisez la première méthode, si vous décidez d'utiliser un flux 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 flux parallèle.

Eran
la source
Je ne suis pas sûr que ce soit un conseil correct: il pourrait utiliser forEachOrderedau lieu de forEachs'il voulait utiliser un flux parallèle tout en conservant l'ordre. Mais en tant que documentation des forEachétats, la préservation de l'ordre des rencontres sacrifie le bénéfice du parallélisme. Je soupçonne que c'est également le cas à l' toListépoque.
herman le
0

Il existe une troisième option - en utilisant stream().toArray()- voir les commentaires sous pourquoi stream n'a pas de 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, donc en l'ajoutant ici au cas où.

en supposant List<String>

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

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

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}

parallèle sans valeurs nulles et filtre (donc le flux est SIZED): toArrays a les meilleures performances dans ce cas, et .forEach()échoue avec "indexOutOfBounds" sur le destinataire 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}
harshtuna
la source
0

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());
Kumar Abhishek
la source
0

Si l'utilisation des bibliothèques 3rd Pary est correcte, cyclops- react définit les collections étendues Lazy avec cette fonctionnalité intégrée. Par exemple, nous pourrions simplement écrire

ListX myListToParse;

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

myFinalList n'est pas évalué avant le premier accès (et après que la liste matérialisée soit mise en cache et réutilisée).

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

John McClean
la source