web-dev-qa-db-fra.com

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

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()); 
146
Emilien Brigand

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 (une opération de filtrage, puis une opération de carte, puis la collecte du résultat), Fowler's excellent article

  3. 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.

124
herman

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()); 
38
assylias

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

  1. Non interférant, ce qui signifie que la fonction ne doit pas modifier la source du flux s'il est non-concurrent (par exemple, ArrayList).
  2. Sans état pour éviter des résultats inattendus lors du traitement en parallèle (dû à des différences de planification de threads).

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.

5
Mika'il

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.

4
Craig P. Motlin

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.

1
Eran

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}
0
harshtuna

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
Kumar Abhishek

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]

0
John McClean