62 votes

Comment utiliser la logique if-else dans Java 8 stream forEach

Ce que je veux faire est montré ci-dessous dans 2 appels de flux. Je veux diviser une collection en 2 nouvelles collections en fonction d'une certaine condition. Idéalement, je veux le faire en 1. J'ai vu des conditions utilisées pour la fonction .map des flux, mais je n'ai rien trouvé pour le forEach. Quel est le meilleur moyen d'atteindre ce que je veux?

   animalMap.entrySet().stream()
            .filter(pair-> pair.getValue() != null)
            .forEach(pair-> myMap.put(pair.getKey(), pair.getValue()));

    animalMap.entrySet().stream()
            .filter(pair-> pair.getValue() == null)
            .forEach(pair-> myList.add(pair.getKey()));

3 votes

On dirait une situation où les flux ne vous aident pas vraiment. Cela cache simplement la syntaxe du flux de contrôle avec une API de manière maladroite et votre lambda forEach est étatique.

105voto

Alex Shesterov Points 3733

Il suffit de mettre la condition directement dans le lambda lui-même, par exemple.

animalMap.entrySet().stream()
        .forEach(
                pair -> {
                    if (pair.getValue() != null) {
                        myMap.put(pair.getKey(), pair.getValue());
                    } else {
                        myList.add(pair.getKey());
                    }
                }
        );

Bien sûr, cela suppose que les deux collections (myMap et myList) sont déclarées et initialisées avant le code ci-dessus.


Mise à jour : en utilisant Map.forEach, le code devient plus court, plus efficace et plus lisible, comme Jorn Vernee l'a gentiment suggéré:

    animalMap.forEach(
            (key, value) -> {
                if (value != null) {
                    myMap.put(key, value);
                } else {
                    myList.add(key);
                }
            }
    );

6 votes

Tu pourrais utiliser Map.forEach à la place, ce serait un peu plus concis.

1 votes

Vous pouvez également utiliser des accolades { ... } dans l'expression lambda, si elle est plus complexe qu'un simple opérateur ternaire peut gérer.

1 votes

Merci pour votre réponse concise :) Hm, j'obtiens "Mauvais type de retour dans l'expression lambda : Serializable & Comparable> ne peut pas être converti en void.

15voto

Holger Points 13789

La plupart du temps, lorsque vous vous retrouvez à utiliser forEach sur un Stream, vous devriez réfléchir à savoir si vous utilisez le bon outil pour votre tâche ou si vous l'utilisez de la bonne manière.

En général, vous devriez rechercher une opération terminale appropriée faisant ce que vous souhaitez accomplir ou un collecteur approprié. Maintenant, il existe des collecteurs pour produire des Maps et des Lists, mais aucun collecteur prêt à l'emploi pour combiner deux collecteurs différents, basés sur un prédicat.

Maintenant, cette réponse contient un collecteur pour combiner deux collecteurs. En utilisant ce collecteur, vous pouvez accomplir la tâche comme suit

Pair, List> pair = animalMap.entrySet().stream()
    .collect(conditional(entry -> entry.getValue() != null,
            Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue),
            Collectors.mapping(Map.Entry::getKey, Collectors.toList()) ));
Map myMap = pair.a;
List myList = pair.b;

Mais peut-être que vous pouvez résoudre cette tâche spécifique de manière plus simple. Un de vos résultats correspond au type d'entrée; c'est la même map juste dépouillée des entrées qui se rapportent à null. Si votre map d'origine est mutable et que vous n'en avez plus besoin par la suite, vous pouvez simplement collecter la liste et supprimer ces clés de la map d'origine car elles sont mutuellement exclusives :

List myList=animalMap.entrySet().stream()
    .filter(pair -> pair.getValue() == null)
    .map(Map.Entry::getKey)
    .collect(Collectors.toList());

animalMap.keySet().removeAll(myList);

Notez que vous pouvez supprimer les mappings vers null même sans avoir la liste des autres clés :

animalMap.values().removeIf(Objects::isNull);

ou

animalMap.values().removeAll(Collections.singleton(null));

Si vous ne pouvez pas (ou ne souhaitez pas) modifier la map d'origine, il existe encore une solution sans collecteur personnalisé. Comme suggéré dans la réponse de Alexis C., partitioningBy va dans la bonne direction, mais vous pouvez simplifier cela :

Map> tmp = animalMap.entrySet().stream()
    .collect(Collectors.partitioningBy(pair -> pair.getValue() != null,
                 Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
Map myMap = tmp.get(true);
List myList = new ArrayList<>(tmp.get(false).keySet());

En fin de compte, n'oubliez pas les opérations ordinaires sur les Collections, vous n'avez pas besoin de tout faire avec la nouvelle API Stream.

0 votes

Holger, n'es-tu pas d'accord que ta solution est définitivement moins lisible que celle acceptée ?

5 votes

@Marco Altieri: cela dépend de la question réelle. La question portait en fait sur l'API Stream, à laquelle la réponse acceptée ne répond pas vraiment, car en fin de compte, forEach est juste une syntaxe alternative pour une boucle for. Par exemple, la variante Map.forEach(…) ne peut pas être exécutée en parallèle, la variante entrySet().stream().forEach(…) va se casser de manière terrible lorsqu'elle est exécutée en parallèle. Si vous voulez utiliser l'API Stream et comprendre comment l'utiliser correctement, vous devez suivre la réponse d'Alexis C ou la mienne. Une fois que vous avez compris, cela ne vous semblera pas illisible...

0 votes

lorsque vous vous retrouvez à utiliser forEach sur un Stream, vous devriez reconsidérer si vous utilisez le bon outil pour votre travail Ceci est une déclaration étrange. Un exemple typique d'un Stream de Collection est un forEach ou un filter.

12voto

ZouZou Points 23600

Le problème en utilisant stream().forEach(..) avec un appel à add ou put à l'intérieur du forEach (donc vous modifiez l'instance externe myMap ou myList) est que vous pouvez facilement rencontrer des problèmes de concurrence si quelqu'un passe le stream en parallèle et que la collection que vous modifiez n'est pas thread safe.

Une approche que vous pouvez prendre est de d'abord partitionner les entrées dans la carte originale. Une fois que vous avez cela, récupérez la liste correspondante d'entrées et collectez-les dans la carte et liste appropriées.

Map>> partitions =
    animalMap.entrySet()
             .stream()
             .collect(partitioningBy(e -> e.getValue() == null));

Map myMap = 
    partitions.get(false)
              .stream()
              .collect(toMap(Map.Entry::getKey, Map.Entry::getValue));

List myList =
    partitions.get(true)
              .stream()
              .map(Map.Entry::getKey) 
              .collect(toList());

... ou si vous préférez le faire en une seule passe, implémentez un collecteur personnalisé (en supposant qu'une classe Tuple2 existe, vous pouvez créer la vôtre), par exemple :

public static  Collector, ?, Tuple2, List>> customCollector() {
    return Collector.of(
            () -> new Tuple2<>(new HashMap<>(), new ArrayList<>()),
            (pair, entry) -> {
                if(entry.getValue() == null) {
                    pair._2.add(entry.getKey());
                } else {
                    pair._1.put(entry.getKey(), entry.getValue());
                }
            },
            (p1, p2) -> {
                p1._1.putAll(p2._1);
                p1._2.addAll(p2._2);
                return p1;
            });
}

avec son utilisation :

Tuple2, List> pair = 
    animalMap.entrySet().parallelStream().collect(customCollector());

Vous pouvez le personnaliser davantage si vous le souhaitez, par exemple en fournissant un prédicat en tant que paramètre.

1 votes

La préoccupation de la concurrence est importante.

9voto

user3337629 Points 36

Je pense que c'est possible en Java 9 :

animalMap.entrySet().stream()
                .forEach(
                        pair -> Optional.ofNullable(pair.getValue())
                                .ifPresentOrElse(v -> myMap.put(pair.getKey(), v), v -> myList.add(pair.getKey()))
                );

J'ai besoin de ifPresentOrElse pour que cela fonctionne. (Je pense qu'une boucle for est mieux).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X