Java 8-Meilleure façon de transformer une liste: carte ou foreach?


J'ai une liste de myListToParse où 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. Je voudrais connaître le moyen le plus efficace entre eux et comprendre pourquoi un moyen est meilleur que l'autre.

Je suis ouvert à toute suggestion sur 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()); 
Author: herman, 2015-02-04

8 answers

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. Il est plus lisible car les différentes étapes effectuées dans le pipeline de collecte sont écrites séquentiellement (d'abord une opération de filtre, puis une opération de carte, puis la collecte du résultat), (pour plus d'info sur le avantages des pipelines de collecte, voir l'excellent article de Martin Fowler )

  3. Vous pouvez facilement modifier la manière dont les valeurs sont collectées en remplaçant le Collector utilisé. Dans certains cas, vous pouvez avoir besoin d'écrire votre propre Collector, mais l'avantage est que vous pouvez facilement réutiliser.

 115
Author: herman, 2015-02-05 23:38:15

Je suis d'accord avec les réponses existantes que la deuxième forme est meilleure car elle n'a pas d'effets secondaires 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, map effectuera vraiment beaucoup mieux. Voir ci-dessous les résultats 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 un terminal méthode - 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.

Enfin, notez que votre deuxième extrait peut être écrit de manière 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()); 
 34
Author: assylias, 2016-01-23 19:02:15

L'un des principaux avantages de l'utilisation de streams 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 exemple de code n'est potentiellement pas fonctionnel car foreach la méthode est classée comme terminale (ce qui signifie qu'elle peut produire des effets secondaires).

La deuxième voie est préférée du point de vue de la programmation fonctionnelle car la fonction map 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 flux si elle est non concurrente (par exemple ArrayList).
  2. Apatride pour éviter les résultats inattendus lors de la traitement parallèle (causé par des différences de planification de threads).

Un autre avantage de la deuxième 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.

 5
Author: Mika'il, 2015-02-04 14:19:47

Si vous utilisez Eclipse Collections vous pouvez utiliser le 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 impatience et devrait être un peu plus rapide que d'utiliser un flux.

Remarque: Je suis un committer pour les collections Eclipse.

 4
Author: Craig P. Motlin, 2017-02-13 20:39:55

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

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

 1
Author: Eran, 2015-02-04 10:34:55

Il existe une troisième option - en utilisant stream().toArray() - voir les commentaires sous pourquoi stream n'avait-il 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 JDK ultérieures, donc en l'ajoutant ici juste 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 null et une transformation simple 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 null et filtre (donc le flux est SIZED): toArrays a les meilleures performances dans ce cas, et .forEach() échoue avec "indexOutOfBounds" sur le réceptif 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}
 0
Author: harshtuna, 2017-05-23 11:54:48

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());
 0
Author: Kumar Abhishek, 2016-04-05 13:56:05

Si l'utilisation de 3rd Pary Libaries est ok cyclops-react définit des collections étendues paresseuses avec cette fonctionnalité intégrée. Par exemple, nous pourrions simplement écrire

ListX myListToParse;

ListX myFinalList = myListToParse.filtre(elt -> elt != NULL) .carte(elt -> doSomething(elt));

MyFinalList n'est pas évalué avant le premier accès (et là après la mise en cache et la réutilisation de la liste matérialisée).

[Divulgation Je suis le chef de file développeur de cyclope-réagir]

 0
Author: John McClean, 2017-03-10 15:53:16