web-dev-qa-db-fra.com

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

Ce que je veux faire est illustré ci-dessous dans 2 appels de flux. Je souhaite diviser une collection en 2 nouvelles collections en fonction de certaines conditions. Idéalement, je souhaite le faire dans 1. J'ai vu les conditions utilisées pour la fonction .map des flux, mais je n'ai rien trouvé pour forEach. Quel est le meilleur moyen de réaliser 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()));
36
user3768533

Il suffit de placer la condition 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 entendu, cela suppose que les deux collections (myMap et myList) soient déclarées et initialisées avant le code ci-dessus.


Mise à jour: en utilisant Map.forEach rend le code plus court, plus efficace et lisible, comme Jorn Vernee aimablement suggéré:

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

Dans la plupart des cas, lorsque vous utilisez forEach sur un flux, vous devez repenser si vous utilisez le bon outil pour votre travail ou si vous l'utilisez de la bonne manière.

En règle générale, vous devez rechercher une opération de terminal appropriée réalisant ce que vous souhaitez atteindre ou un collecteur approprié. Maintenant, il existe des collecteurs pour produire Maps et Lists, mais pas de collecteur prêt à l'emploi pour combiner deux collecteurs différents, basés sur un prédicat.

Maintenant, cette réponse contient un collecteur permettant de combiner deux collecteurs. En utilisant ce collecteur, vous pouvez réaliser la tâche de la manière suivante:

Pair<Map<KeyType, Animal>, List<KeyType>> 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<KeyType,Animal> myMap = pair.a;
List<KeyType> myList = pair.b;

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

List<KeyType> 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 mappages 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 voulez pas) modifier la carte d'origine, il existe toujours une solution sans collecteur personnalisé. Comme indiqué dans la réponse d’Alexis C. , partitioningBy va dans la bonne direction, mais vous pouvez le simplifier:

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

En fin de compte, n’oubliez pas les opérations de Collection ordinaires, vous n’avez pas à tout faire avec la nouvelle API Stream.

11
Holger

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

Une approche possible consiste à partitionner d'abord les entrées dans la carte d'origine. Une fois que vous avez cela, prenez la liste des entrées correspondantes et rassemblez-les dans la carte et la liste appropriées.

Map<Boolean, List<Map.Entry<K, V>>> partitions =
    animalMap.entrySet()
             .stream()
             .collect(partitioningBy(e -> e.getValue() == null));

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

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

... ou si vous voulez le faire en un seul passage, implémentez un collecteur personnalisé (en supposant qu'une classe Tuple2<E1, E2> existe, vous pouvez créer le vôtre), par exemple:

public static <K,V> Collector<Map.Entry<K, V>, ?, Tuple2<Map<K, V>, List<K>>> 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<Map<K, V>, List<K>> pair = 
    animalMap.entrySet().parallelStream().collect(customCollector());

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

7
Alexis C.

Je pense que c'est possible dans Java 9:

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

Besoin du ifPresentOrElse pour que cela fonctionne bien. (Je pense qu'une boucle for semble meilleure.)

6
user3337629