Java 8 Streams 可以对集合中的项目进行操作,然后将其删除吗?

Posted

技术标签:

【中文标题】Java 8 Streams 可以对集合中的项目进行操作,然后将其删除吗?【英文标题】:Can Java 8 Streams operate on an item in a collection, and then remove it? 【发布时间】:2015-07-14 11:49:18 【问题描述】:

与几乎所有人一样,我仍在学习新的 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 的人看到类似的东西?

【问题讨论】:

【参考方案1】:

你可以这样做:

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

【讨论】:

+1 用于使用 for 循环;不要只使用流,以便您可以将代码放在一行中。循环通常比流解决方案更具可读性,即使它们更冗长。 我有点犹豫是否将状态更改代码插入removeIf(),尽管我相信这在理论上可行。此外,我的情况适合函数式编程,所以我对流的需求不仅仅是将所有内容打包到一行中;但谢谢。 removeIf 与 lambda 表达式一起使用是完全合法的,并且应该以这种方式使用。状态更改是集合 API 的一部分,而不是流。 for 循环的好例子,但是为什么你认为它更清楚?对我来说,我实际上认为,在这种情况下,流解决方案更清晰 我真的很喜欢那个迭代器 sn-p。【参考方案2】:

在一行中没有,但也许你可以使用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 在哪里?我在这里错过了什么?【参考方案3】:

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

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

祝你好运,编码愉快:)

【讨论】:

请注意,这将仅删除与谓词匹配的第一个匹配项【参考方案4】:

操作后,它们不再有任何用途,应从原始集合中删除。代码运行良好,但我无法摆脱这样的感觉,即 Stream 中有一个操作可以在一行中为我完成此操作。

您不能使用流从流的源中删除元素。来自Javadoc:

大多数流操作接受描述用户指定行为的参数.....为了保持正确的行为,这些行为参数:

必须不干扰(它们不会修改流源);和 在大多数情况下必须是无状态的(它们的结果不应依赖于在流管道执行期间可能更改的任何状态)。

【讨论】:

【参考方案5】:

你真正想要做的是对你的集合进行分区。不幸的是,在 Java 8 中,只能通过终端“收集”方法进行分区。你最终会得到这样的结果:

// 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”方法。如果我这样做,我认为我的代码不会更清晰或更快。感谢您的帮助! 令人惊讶的是,您不需要这样的特定方法。 Scala 中的相同代码更有意义(见上文)。 答案中的remove 在哪里?我在这里错过了什么? @AlikElzin-kilaka List partitioned.get(false) 中的元素是removed 元素,partitioned.get(true) 中的列表是@987654326 之后的原始List 的剩余部分@al。如果我做对了。【参考方案6】:

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

【讨论】:

我一直在考虑这个问题,但是对于我的实际(未简化)实现,流可能会变得非常庞大。我通常在四到八核机器上运行它;因此,尽管 Java 的 Stream API 很年轻,但我还不相信迭代器会带来好处。 parallel() 可能是这里的救星。不过,感谢您的及时回复!【参考方案7】:

如果我正确理解您的问题:

set = set.stream().filter(item -> 
    if (item.qualify()) 
        ((Qualifier) item).operate();
        return false;
    
    return true;
).collect(Collectors.toSet());

【讨论】:

【参考方案8】:

我看到保罗在使用流时的清晰度问题,在最佳答案中说明了这一点。也许添加解释变量可以稍微阐明意图。

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

【讨论】:

以上是关于Java 8 Streams 可以对集合中的项目进行操作,然后将其删除吗?的主要内容,如果未能解决你的问题,请参考以下文章

Java 8 中的 Streams API 详解

Java 8 中的 Streams API 详解

Java 8 中的 Streams API 详解

Java 8 中的 Streams API 详解

Java 8 中的 Streams API

Java 8 中的 Streams API 详解