Java 8的Stream能操作集合中的元素并将其删除吗?

72

和大多数人一样,我仍然在学习并且喜欢使用新的Java 8 Streams API。我有一个关于流使用的问题,下面提供了一个简化的例子。

Java Streams允许我们将Collection作为参数,并使用其中的stream()方法来获得其所有元素的流。在其中,有许多有用的方法,例如filter()map()forEach(),它们允许我们对内容使用lambda操作。

我有一段类似如下的代码(简化版):

set.stream().filter(item -> item.qualify())
    .map(item -> (Qualifier)item).forEach(item -> item.operate());
set.removeIf(item -> item.qualify());

这个想法是获取符合特定限定条件的集合中所有项目的映射,然后对它们进行操作。操作完成后,它们将不再有任何用途,并应从原始集合中删除。代码运行良好,但我感觉 Stream 中可能有一种操作可以在一行内完成这个任务。

如果有在 Javadocs 中,请指出来,我可能会忽略了。

有没有更熟悉该 API 的人看到类似的操作呢?

8个回答

124

你可以这样做:

set.removeIf(item -> {
    if (!item.qualify())
        return false;
    item.operate();
    return true;
});
如果item.operate()总是返回true,那么你可以非常简洁地完成它。
set.removeIf(item -> item.qualify() && item.operate());

然而,我不喜欢这些方法,因为它们不够清晰易懂。就我个人而言,我会继续使用 for 循环和 Iterator

for (Iterator<Item> i = set.iterator(); i.hasNext();) {
    Item item = i.next();
    if (item.qualify()) {
        item.operate();
        i.remove();
    }
}

12
使用for循环加1;不要只是为了让代码一行放得下而使用流。即使它们更长,循环通常比流式解决方案更易读。请注意,这里没有任何解释。 - dimo414
4
尽管我相信这理论上是可行的,但我还是有些犹豫在removeIf()中插入改变状态的代码。此外,我的情况适合函数式编程,所以我对流的需求不仅仅是为了将所有内容都压缩到一行中;不过还是谢谢你。 - Michael Macha
3
使用lambda表达式的removeIf是完全合法的,并且应该这样使用。修改状态是集合API的一部分,而不是流API的一部分。 - hussachai
7
循环中的良好示例,但您认为它更清晰的原因是什么?对我来说,实际上我认为,在这种情况下,流解决方案更清晰。 - serup
我真的很喜欢那个迭代器片段。 - Kamil

4

一句话回答是否可以,不行,但是也许你可以使用partitioningBy收集器:

Map<Boolean, Set<Item>> map = 
    set.stream()
       .collect(partitioningBy(Item::qualify, toSet()));

map.get(true).forEach(i -> ((Qualifier)i).operate());
set = map.get(false);

由于避免了对集合进行两次迭代,一次用于筛选流,另一次用于删除相应的元素,因此这种方法可能更有效率。

否则,我认为你的方法相对不错。


答案中的 remove 在哪里?我在这里漏掉了什么? - AlikElzin-kilaka

4

有很多方法。如果您使用myList.remove(element),则必须覆盖equals()。我更喜欢的方法是:

allList.removeIf(item -> item.getId().equals(elementToDelete.getId()));

祝你好运,编程愉快 :)


请注意,这将仅删除与谓词匹配的第一个出现。 - arxakoulini

3
操作完成后,它们已经没有任何作用,应从原始集合中移除。代码运行良好,但我有一种感觉,在Stream中有一个操作可以在单行中完成此操作。

使用Stream无法从源流中删除元素。来自Javadoc的说明:

大多数流操作接受描述用户指定行为的参数..... 为了保持正确的行为,这些行为参数:
  • 必须是非干扰性的(它们不修改流源); 和
  • 在大多数情况下,必须是无状态的(它们的结果不应该依赖于在执行流管道期间可能会改变的任何状态)。

2

您真正想做的是对集合进行分区。不幸的是,在Java 8中,只能通过终端“collect”方法进行分区。最终结果可能像这样:

// test data set
Set<Integer> set = ImmutableSet.of(1, 2, 3, 4, 5);
// predicate separating even and odd numbers
Predicate<Integer> evenNumber = n -> n % 2 == 0;

// initial set partitioned by the predicate
Map<Boolean, List<Integer>> partitioned = set.stream().collect(Collectors.partitioningBy(evenNumber));

// print even numbers
partitioned.get(true).forEach(System.out::println);
// do something else with the rest of the set (odd numbers)
doSomethingElse(partitioned.get(false))

更新:

上述代码的Scala版本

val set = Set(1, 2, 3, 4, 5)
val partitioned = set.partition(_ % 2 == 0)
partitioned._1.foreach(println)
doSomethingElse(partitioned._2)`

希望有一天他们能够实现一个“forEachAndRemove”方法。我认为如果这样做,我的代码不会更清晰,也不会更快。感谢您的帮助! - Michael Macha
令人惊讶的是,你不需要像那样使用特定的方法。在Scala中,相同的代码更有意义(见上文)。 - Dmitriy Yefremov
答案中的 remove 在哪里?我在这里漏掉了什么? - AlikElzin-kilaka
@AlikElzin-kilaka,分区列表中partitioned.get(false)的元素是已经被“删除”的,而partitioned.get(true)中的列表则是在“删除”操作后原始列表中剩余的部分。如果我理解正确的话。 - Alain BECKER

1
不,你的实现可能是最简单的。您可能会通过修改removeIf谓词中的状态来执行一些非常邪恶的操作,请不要这样做。另一方面,实际上切换到基于迭代器的命令式实现可能是合理的,这可能更适用和有效。

我一直在考虑这个问题,但对于我的实际(未简化的)实现来说,流可能会变得非常庞大。我通常在四到八核机器上运行此代码;因此,尽管Java的Stream API还很年轻,但我仍然不确定迭代器是否有益。parallel()可能会成为救星。感谢您的快速回复! - Michael Macha

1
如果我正确理解了您的问题:
set = set.stream().filter(item -> {
    if (item.qualify()) {
        ((Qualifier) item).operate();
        return false;
    }
    return true;
}).collect(Collectors.toSet());

0

我看到了在顶部答案中关于使用流的Paul的明确担忧。或许加上解释变量可以稍微澄清意图。

set.removeIf(item -> {
  boolean removeItem=item.qualify();
  if (removeItem){
    item.operate();
  }
  return removeItem;
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接