Как использовать логику if-else в потоке Java 8 для каждого

То, что я хочу сделать, показано ниже в двух потоковых вызовах. Я хочу разбить коллекцию на две новые коллекции, основанные на некоторых условиях. В идеале я хочу сделать это в 1. Я видел условия, используемые для функции .map потоков, но не мог найти ничего для forEach. Каков наилучший способ достичь того, что я хочу?

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

Ответы

Ответ 1

Просто поставьте условие в сам лямбда, например

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

Конечно, это предполагает, что обе коллекции (myMap и myList) объявляются и инициализируются до вышеуказанного фрагмента кода.


Обновление: с помощью Map.forEach делает код короче, а также более эффективным и удобочитаемым, поскольку Jorn Vernee любезно предложил:

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

Ответ 2

Проблема с помощью stream().forEach(..) с вызовом add или put внутри forEach (так что вы мутируете внешний экземпляр myMap или myList), вы можете легко запускать в concurrency если кто-то поворачивает поток параллельно, а коллекция, которую вы изменяете, не является потокобезопасной.

Один из подходов, который вы можете предпринять, - это сначала разбить записи на исходной карте. После этого возьмите соответствующий список записей и соберите их в соответствующей карте и списке.

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

... или если вы хотите сделать это за один проход, выполните пользовательский сборщик (если существует класс Tuple2<E1, E2>, вы можете создать свой собственный), например:

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;
            });
}

с его использованием:

Tuple2<Map<K, V>, List<K>> pair = 
    animalMap.entrySet().parallelStream().collect(customCollector());

Вы можете настроить его, если хотите, например, предоставив предикат в качестве параметра.

Ответ 3

В большинстве случаев, когда вы используете forEach в потоке, вы должны переосмыслить, используете ли вы подходящий инструмент для своей работы или используете ли вы правильный путь.

Как правило, вы должны искать подходящую операцию терминала, выполняя то, что вы хотите достичь, или для соответствующего Коллектора. Теперь существуют коллекторы для создания Map и List s, но не из коллекционного коллектора для объединения двух разных коллекционеров на основе предиката.

Теперь этот ответ содержит сборщик для объединения двух коллекционеров. Используя этот сборщик, вы можете выполнить задачу как

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;

Но, может быть, вы можете решить эту конкретную задачу более простым способом. Один из результатов соответствует типу ввода; его же карта просто удалила записи, которые отображаются на null. Если ваша исходная карта изменена, и вам это не понадобится впоследствии, вы можете просто собрать список и удалить эти ключи с исходной карты, поскольку они являются взаимоисключающими:

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

animalMap.keySet().removeAll(myList);

Обратите внимание, что вы можете удалить сопоставления в null даже без списка других клавиш:

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

или

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

Если вы не можете (или не хотите) изменять исходную карту, все равно существует решение без пользовательского коллектора. Как указано в ответ Alexis C.s, partitioningBy идет в правильном направлении, но вы можете упростить его:

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

Суть в том, что не забывайте о обычных операциях Collection, вам не нужно делать все с помощью нового Stream API.