无法在一个流中使用filter->forEach->collect吗?

43

我希望能够实现类似于这样的效果:

items.stream()
    .filter(s-> s.contains("B"))
    .forEach(s-> s.setState("ok"))
.collect(Collectors.toList());

筛选,然后更改筛选结果的一个属性,最后将结果收集到列表中。但是调试器显示:

无法在基本类型 void 上调用 collect(Collectors.toList())

我需要使用2个流吗?


1
这里正确的做法是将所选条目收集到目标集合中,然后执行 results.forEach(...) 来执行你的副作用。 - Brian Goetz
4
针对通过谷歌搜索“forEach then collect”的读者,快速回答是使用 peek 方法。该方法可用于遍历流中的每个元素,并允许您执行副作用操作,例如打印或记录元素。 - izogfif
8个回答

46

forEach 是被设计成终止操作的,是的——在调用它之后你不能再做任何事情。

惯用的方式是首先应用变换,然后使用 collect() 将所有内容收集到所需的数据结构中。

变换可以使用 map 进行,该方法专门用于非变异操作。

如果您正在执行非变异操作:

 items.stream()
   .filter(s -> s.contains("B"))
   .map(s -> s.withState("ok"))
   .collect(Collectors.toList());

其中withState是一个方法,返回包括提供的更改在内的原始对象的副本。


如果您正在执行副作用:

items.stream()
  .filter(s -> s.contains("B"))
  .collect(Collectors.toList());

items.forEach(s -> s.setState("ok"))

2
根据JavaDoc,peek应该用于调试:https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#peek-java.util.function.Consumer-另请参阅https://dev59.com/XFwX5IYBdhLWcg3wlwWQ - user140547
1
根据流语义,不应该完全执行突变副作用,但并非总是如此。 - Grzegorz Piwowarek
@user140547,合约行为无法更改,因为这会破坏向后兼容性,即使该方法存在于“主要用于调试”的情况下,“不知道此功能”的人应首先阅读文档。 - Grzegorz Piwowarek
2
在JDK 9中,peek()的规范已经明确说明实现可以优化掉peek() - Brian Goetz
@Eugene,我认为你基本上想让peek()像OP所要求的那样工作--“像forEach()一样,但是是一个中间操作”,这样你就可以在流中自由地进行副作用。 但这不是它的目的。 放弃这个想法,你就没问题了。 - Brian Goetz
显示剩余3条评论

21

使用map替换forEach

 items.stream()
      .filter(s-> s.contains("B"))
      .map(s-> {s.setState("ok");return s;})
      .collect(Collectors.toList());

forEachcollect都是终止操作 - 流只能有一个。任何返回Stream<T>的操作都是中间操作,其他所有操作都是终止操作。


如果您需要流的顺序处理,这个能行吗? - alex
@alex 确切地说,这个例子:是的。 - Eugene
Sonar会抱怨这个,因为它说:风格 - 方法返回修改后的参数 该方法似乎修改了一个参数,然后将此参数作为方法的返回值返回。这将使调用者混淆,因为不明显的是“原始”的传入参数也将被更改。如果该方法的目的是更改参数,则更改方法以具有void返回值将更清晰。如果由于接口或超类合同需要返回类型,则可以创建参数的克隆。 - SebastianX

6

抵制在没有很好的理由的情况下在流内部使用副作用的冲动。创建新列表,然后应用更改:

List<MyObj> toProcess = items.stream()
    .filter(s -> s.contains("B"))
    .collect(toList());

toProcess.forEach(s -> s.setState("ok"));

peek 是用于副作用的。 - Grzegorz Piwowarek
@GrzegorzPiwowarek ...但是预测它将在哪些元素上调用有点棘手,而且这在Java 9中也不会变得更容易(它允许流实现进行更多优化)。您需要检查上游和下游操作(filterdistinct,短路终端操作如count()anyMatch()findFirst()等)才能确定。对于像这样的简单情况,这可能是可以接受的,但通常最好避免依赖它。 - Hulk
@ Hulk 我知道这一点 - 我希望在使用应该访问所有元素的方法(如上面提到的方法)终止流时,不会有任何方式可以跳过 peek() 调用。很遗憾需要分两步完成。 - Grzegorz Piwowarek
@GrzegorzPiwowarek 嗯,JavaDocs的草稿中明确提到了countfindFirst(“在流实现能够优化掉某些或所有元素的生成的情况下(例如使用findFirst这样的短路操作,或者在count()中描述的示例中),对于那些元素不会调用该操作。”),几天后我们就会确定最终版本的措辞是什么。 - Hulk

6

forEach是一个终端操作,这意味着它产生的结果不是流。 forEach 不会产生任何东西,而 collect 返回一个集合。你需要的是一种流操作,可以根据你的需求修改元素。这个操作是 map,它允许你指定一个函数来应用于输入流的每个元素,并产生一个转换后的元素流。所以你需要像下面这样的东西:

items.stream()
     .filter (s -> s.contains("B"))
     .map    (s -> { s.setState("ok"); return s; }) // need to return a value here
     .collect(Collectors.toList());

另一种方法是使用 peek,其意图是在遍历每个元素时应用函数(但其主要目的是进行调试):

items.stream()
     .filter (s -> s.contains("B"))
     .peek   (s -> s.setState("ok")) // no need to return a value here
     .collect(Collectors.toList());

当使用上述代码中的“map”时,调试器会显示:“无法推断<R> map(Function<? super T,? extends R>)的类型参数”。改用“peek”可以解决此问题。因此,我认为最好在这种情况下使用“peek”。 - nimo23
使用“map”时,必须返回实例:“.map(s-> {s.setState(“ok”); return s;})”。这样,它就像“peek”一样工作。但是,“peek”不能返回值。 - nimo23
需要稍微修改,抱歉。因为函数需要为一个映射返回一个值... 感谢@nimo23,我写得太快了。 - Jean-Baptiste Yunès

4

您不能在同一个流上执行两个终端操作。

您可以在中间操作(例如map)中设置对象的状态:

List<YourClass> list = 
    items.stream()
         .filter(s-> s.contains("B"))
         .map(s-> {
                      s.setState("ok"); 
                      return s;
                  })
         .collect(Collectors.toList());

风格 - 方法返回修改后的参数 代码异味 FindBugs Contrib(Java) 此方法似乎修改了一个参数,然后将此参数作为方法的返回值返回。这会令调用者困惑,因为不清楚“原始”的传入参数也将被更改。如果此方法的目的是更改参数,则更改方法使其具有void返回值会更清晰。如果由于接口或超类契约需要返回类型,则可以考虑对参数进行克隆。 - Sonar将抱怨 - SebastianX

4
 items.stream()
      .filter(s-> s.contains("B"))
      .peek(s-> s.setState("ok"))
      .collect(Collectors.toList());

Stream peek(Consumer action) Returns a stream consisting of the elements of this stream, additionally performing the provided action on each element as elements are consumed from the resulting stream. This is an intermediate operation.

For parallel stream pipelines, the action may be called at whatever time and in whatever thread the element is made available by the upstream operation. If the action modifies shared state, it is responsible for providing the required synchronization.

API Note: This method exists mainly to support debugging, where you want to see the elements as they flow past a certain point in a pipeline:

 Stream.of("one", "two", "three", "four")
     .filter(e -> e.length() > 3)
     .peek(e -> System.out.println("Filtered value: " + e))
     .map(String::toUpperCase)
     .peek(e -> System.out.println("Mapped value: " + e))
     .collect(Collectors.toList());   Parameters: action - a non-interfering action to perform on the elements as they are consumed

from the stream Returns: the new stream

https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#peek-java.util.function.Consumer-


您提供的文档明确说明仅应将此方法用于调试。我不会将其用于副作用。 - Markus Rohlof

1
请使用.peek()代替.forEach()
items.stream()
    .filter(s-> s.contains("B"))
    .peek(s-> s.setState("ok"))
.collect(Collectors.toList());

0
public static void main(String[] args) {
    // Create and populate the Test List
    List<Object> objectList = new ArrayList<>();
    objectList.add("s");
    objectList.add(1);
    objectList.add(5L);
    objectList.add(7D);
    objectList.add(Boolean.TRUE);

    // Filter by some condition and collect
    List<Object> targetObjectList = 
        objectList.stream().filter(o -> o instanceof String)
        .collect(Collectors.toList());

    // Check
    targetObjectList.forEach(System.out::println);
}

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