Cannot make filter->forEach->collect in one stream

2020-06-07 05:44发布

问题:

I want to achieve something like this:

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

filter, then change a property from the filtered result, then collect the result to a list. However, the debugger says:

Cannot invoke collect(Collectors.toList()) on the primitive type void.

Do I need 2 streams for that?

回答1:

The forEach is designed to be a terminal operation and yes - you can't do anything after you call it.

The idiomatic way would be to apply a transformation first and then collect() everything to the desired data structure.

The transformation can be performed using map which is designed for non-mutating operations.

If you are performing a non-mutating operation:

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

where withState is a method that returns a copy of the original object including the provided change.


If you are performing a side effect:

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

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


回答2:

Replace forEach with map.

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

forEach and collect are both terminal operations - Streams must have just one. Anything that returns a Stream<T> is a intermediate operation, anything other is a terminal operation.



回答3:

forEach is a terminal operation, means that it produces non-stream result. forEach doesn't produces anything and collect returns a collection. What you need is a stream operation that modifies elements for your needs. This operation is map which lets you specify a function to be applied to each element of the input stream and produces a transformed stream of elements. So you need something like:

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

An alternative is to use peek whose intention is to apply a function to each element traversing (but its main purpose is for debugging):

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


回答4:

Resist the urge to use side-effects from inside the stream without a very good reason. Make the new list and then apply the changes:

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

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


回答5:

You cannot execute two terminal operations on the same Stream.

You can set the state of the object in an intermediate operation, such as map:

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


回答6:

 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-



回答7:

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