【问题标题】:Remove and collect elements with Java streams使用 Java 流删除和收集元素
【发布时间】:2023-03-25 09:41:01
【问题描述】:

假设我有一个Collection 和一个Predicate,它们与我想从Collection 中删除的元素相匹配。但我不只是想丢弃它们,我想将匹配的元素移动到一个新的集合中。我会在 Java 7 中执行类似 this 的操作:

List<E> removed = new LinkedList<>();
for (Iterator<E> i = data.iterator(); i.hasNext();) {
    E e = i.next();
    if (predicate.test(e)) {
        removed.add(e);
        i.remove();
    }
}

我很好奇是否有流/Java 8 方式来做到这一点。不幸的是,Collections.removeIf() 只是返回了一个boolean(甚至没有计算删除元素的数量?太糟糕了。)我设想这样的事情(当然.removeAndYield(Predicate) 不存在):

List<E> removed = data.removeAndYield(predicate).collect(Collectors.toList());

注意:这个问题的灵感来自a similar question;这个问题是关于从集合中删除的项目上获取流的更一般情况。正如链接问题中所指出的,命令式解决方案可能更具可读性,但我很好奇这是否甚至可以通过流实现。

编辑: 显然,我们可以将任务分成两个独立的步骤,并假设适当的数据结构将是有效的。问题是这可以在任意集合上完成(可能没有有效的.contains() 等)。

【问题讨论】:

  • 困难在于你不能在没有ConcurrentModificationException 的情况下迭代它时从Set 中删除元素,因此任何Java 8 方式都需要至少两次迭代,如Misha 的回答。 AFAIK 通过 set 的单次迭代来做到这一点的唯一方法是使用显式的 Iterator
  • 您可以在Collection 上使用Iterator,这表明在概念上可以使用Stream。显然,这并不意味着它是开箱即用的,但它可能并不复杂。或者也许有一个很好的理由它在 JDK 中不可用。
  • 我的问题中的“Java 7”示例演示了在迭代过程中删除元素。如果我们可以通过命令式的方式一次性完成,是否可以在功能上完成?
  • @dimo414 如果您关心性能,请记住,最常用的集合 (ArrayList) 在迭代解决方案下的扩展性非常差——从中间删除元素的成本很高数组通过迭代器一次一个。

标签: java java-8 java-stream


【解决方案1】:

我会保持简单:

Set<E> removed = set.stream()
    .filter(predicate)
    .collect(Collectors.toSet());

set.removeAll(removed);

【讨论】:

  • 我喜欢这个 (+1),尽管您的 3 个选项中的最后 2 个比第一个要好,因为它们只迭代 removed 而不是所有 set
  • 我同意原始结构是集合的情况。但是,如果源结构不是集合,remove.forEach 只会从集合中取出匹配元素的第一个匹配项,而removeIfremoveAll 会将它们全部删除。总的来说,我认为removeAll 是最好的选择。
  • @pbabcdefp 我修改了答案以删除不必要的选项。
【解决方案2】:

如果您想要一种功能性的方式来执行此操作,您可以编写自己的方法。

static <E> Set<E> removeIf(Collection<? extends E> collection, Predicate<? super E> predicate) {
    Set<E> removed = new HashSet<>();
    for (Iterator<? extends E> i = collection.iterator(); i.hasNext();) {
        E e = i.next();
        if (predicate.test(e)) {
            removed.add(e);
            i.remove();
        }
    }
    return removed;
}

这可用于从List 中删除所有奇数。

Set<Integer> set = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5, 6));
Set<Integer> removed = removeIf(set, i -> i % 2 != 0);
System.out.println(set);
System.out.println(removed);

【讨论】:

    【解决方案3】:

    如果你不介意,让我稍微改变一下你的要求。 :-)

    期望结果的一个特点是匹配的元素应该在一个集合中结束,而不匹配的元素应该在不同的集合中结束。在 Java-8 之前的变异世界中,考虑获取不匹配元素集合的最简单方法是从原始集合中删除匹配元素。

    但是删除——对原始列表的修改——是需求的内在部分吗?

    如果不是,则可以通过简单的分区操作来实现:

    Map<Boolean, List<E>> map = data.stream().collect(partitioningBy(predicate));
    

    结果图本质上是两个列表,分别包含匹配的(key = true)和不匹配的(key = false)元素。

    优点是这项技术可以一次性完成,必要时可以并行完成。当然,与从原始元素中删除匹配项相比,这会创建不匹配元素的重复列表,但这是为不变性付出的代价。权衡可能是值得的。

    【讨论】:

    • 好建议。可变集合通常比它们的价值更麻烦,所以很高兴看到不可变集合有一种简单的方法来做到这一点。
    • @dimo414 谢谢,哦,是的,感谢您将 groupingBy 修复为 partitioningBy。糟糕!
    【解决方案4】:

    给自己写一个这样的可重用函数:

    /**
     * Removes all matching element from iterator
     * 
     * @param it
     * @param predicate
     */
    public static <E> void removeMatching(final Iterator<E> it, final Predicate<E> predicate) {
        while (it.hasNext()) {
            final E e = it.next();
            if (predicate.test(e)) {
                it.remove();
            }
        }
    }
    

    我还没有找到现有的 Streams 解决方案。使用 iterator.remove() 需要的内存少于临时集。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-05-05
      • 2020-08-12
      • 1970-01-01
      相关资源
      最近更新 更多