Java 8 - la meilleure façon 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 ajouter le résultat dans une autre liste myFinalList .

avec Java 8 j'ai remarqué que je peux le faire de 2 façons différentes. Je voudrais connaître la manière la plus efficace de les séparer et comprendre pourquoi une manière est meilleure 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()); 
135
demandé sur herman 2015-02-04 13:30:05

8 réponses

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

Méthode 2 est préférable parce que

  1. il ne nécessite pas la mutation d'une collection qui existe en dehors de l'expression lambda,

  2. c'est plus lisible parce que les différentes étapes qui sont effectuées dans le pipeline de collecte sont écrites séquentiellement (d'abord un filtre l'opération, puis une opération de carte, puis à recueillir la suite), (pour plus d'information sur les avantages des pipelines de collecte, voir Martin Fowler excellent article )

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

115
répondu herman 2015-02-05 23:38:15

je suis d'accord avec les réponses existantes que la deuxième forme est meilleure parce qu'elle n'a pas d'effets secondaires et est plus facile à paralléliser (il suffit d'utiliser un flux parallèle).

Performance sage, il semble qu'ils sont l'équivalent jusqu'à ce que vous commencez à l'aide de flux parallèles. Dans ce cas, map sera 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 boost le premier exemple de la même manière parce que forEach est une méthode terminale - il retourne vide - donc vous êtes forcé d'utiliser un lambda stateful. 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 d'une manière plus concise et 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
répondu assylias 2016-01-23 19:02:15

L'un des principaux avantages de l'utilisation de streams est qu'il donne la capacité de traiter des données d'une manière déclarative, c'est-à-dire, en utilisant un style fonctionnel de programmation. Il donne également la capacité de multi-threading pour le sens libre il n'y a pas besoin 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 est que vous voulez exploiter ces avantages, alors votre premier échantillon de code est potentiellement non fonctionnelle puisque la méthode foreach 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 puisque la fonction de carte peut accepter des fonctions lambda apatrides. Plus explicitement, la lambda passée à la fonction map devrait être

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

un autre avantage de la deuxième approche est que si le flux est parallèle et que le collecteur est concurrent et non classé, alors ces caractéristiques peuvent fournir des indications utiles à l'opération de réduction pour effectuer la collecte concurremment.

5
répondu Mika'il 2015-02-04 14:19:47

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

Note: je suis un committer pour les Collections Eclipse.

4
répondu Craig P. Motlin 2017-02-13 20:39:55

je préfère la seconde.

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 préservera l'ordre même si vous utilisez un flux parallèle.

1
répondu Eran 2015-02-04 10:34:55

il y a une troisième option - en utilisant stream().toArray() - voir les commentaires sous pourquoi le flux n'a pas une méthode toliste . Il s'avère être plus lent que forEach () ou collect (), et moins expressif. Il pourrait être optimisé dans les constructions ultérieures de JDK, donc en l'ajoutant ici juste au cas où.

supposant 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% nulls et simple transform in 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équentielle:

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 nulls et filtre (donc le flux est SIZED ): toArrays a la meilleure performance dans un tel cas, et .forEach() échoue avec "indexOutOfBounds" sur le tableau de réception, 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
répondu 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
répondu Kumar Abhishek 2016-04-05 13:56:05

Si l'aide de 3ème Pary Libaries est ok cyclope-réagir définit Paresseux étendue des collections 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é jusqu'à ce que le premier accès (et là après la matérialisation la liste est mise en cache et réutilisés).

[révélation je suis le développeur principal de cyclope-réagir]

0
répondu John McClean 2017-03-10 15:53:16