【问题标题】:How to use if-else logic in Java 8 stream forEach如何在 Java 8 流 forEach 中使用 if-else 逻辑
【发布时间】:2016-06-24 19:48:04
【问题描述】:

下面的 2 个流调用中显示了我想要做的事情。我想根据某些条件将一个集合拆分为 2 个新集合。理想情况下,我想在 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()));

【问题讨论】:

  • 似乎流实际上并没有给你带来任何好处。它只是用 API 隐藏控制流语法,结果很尴尬,而且你的 forEach lambda 是有状态的。

标签: java java-8 java-stream


【解决方案1】:

只需将条件放入 lambda 本身,例如

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

当然,这假设两个集合(myMapmyList)在上述代码之前已声明和初始化。


更新:使用Map.forEach 使代码更短、更高效、更易读,正如Jorn Vernee 所建议的那样:

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

【讨论】:

  • 你可以改用Map.forEach,这样会更简洁一些。
  • 你也可以在 lambda 表达式中使用大括号 { ... },如果它不是一个简单的三元可以处理的话。
  • 感谢您简洁的回复 :) 嗯,我收到“lambda 表达式中的错误返回类型:Serializable & Comparable extends Serializable & Comparable>> 无法转换为 void。”跨度>
  • @user3768533,更新了解决此问题的答案。我的错,对不起,从第一次开始就没有小心。现在编译并测试了代码。编译失败的原因是三元运算符是一个错误的选择,如果它是关于纯代码流(而不是表达式)。 Here the relevant SO question on this topic.
  • @ShivangAgarwal,这超出了这个问题的范围,而且真的很微不足道。没有必要用空检查污染答案。请注意,myMapmyList 也可以为空。
【解决方案2】:

在大多数情况下,当您发现自己在 Stream 上使用 forEach 时,您应该重新考虑是否为您的工作使用了正确的工具,或者您是否以正确的方式使用它。

通常,您应该寻找合适的终端操作来完成您想要实现的目标,或者寻找合适的收集器。现在,有用于生成 Maps 和 Lists 的收集器,但没有用于基于谓词组合两个不同收集器的开箱即用收集器。

现在,this answer 包含一个用于组合两个收集器的收集器。使用此收集器,您可以完成任务

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 answer 中所暗示的,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 做所有事情。

【讨论】:

  • Holger,您不同意您的解决方案的可读性肯定不如公认的解决方案吗?
  • @Marco Altieri:这取决于实际问题。这个问题实际上是关于 Stream API 的,而接受的答案并没有真正回答,因为最后,forEach 只是for 循环的替代语法。例如,Map.forEach(…) 变体不能并行运行,entrySet().stream().forEach(…) 变体在并行运行时会严重损坏。当您想使用 Stream API 并了解如何正确使用它时,您必须使用 Alexis C 的答案或我的答案。一旦你理解了,你就不会觉得它不可读……
  • when you find yourself using forEach on a Stream, you should rethink whether you are using the right tool for your job 这是一个奇怪的说法。集合流的典型示例是 forEachfilter
  • @geneb。不,不是。典型情况是带有结果的查询,forEach 是一个不产生结果的终端操作。大多数使用forEach 而不是其他终端操作之一来解决问题的尝试都是变相的循环,并且使用循环会更好地工作。在示例代码(如 Stackoverflow 上)或测试代码中,您可能会发现 forEach 带有打印语句,但严重的应用程序代码不包含打印语句。
【解决方案3】:

使用stream().forEach(..) 并在forEach 内部调用addput 的问题是您可以很容易地遇到并发问题如果有人并行打开流并且您正在修改的集合不是线程安全的。

您可以采取的一种方法是首先对原始映射中的条目进行分区。一旦你有了它,获取相应的条目列表并将它们收集到适当的地图和列表中。

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&lt;E1, E2&gt; 类,您可以创建自己的),例如:

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

您可以根据需要对其进行更多调整,例如通过提供谓词作为参数。

【讨论】:

  • 并发问题很重要。
【解决方案4】:

我认为这在 Java 9 中是可能的:

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

需要 ifPresentOrElse 才能工作。 (我认为 for 循环看起来更好。)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-03-24
    相关资源
    最近更新 更多