nimo23
nimo23

Reputation: 5708

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

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?

Upvotes: 44

Views: 95899

Answers (8)

Leena
Leena

Reputation: 783

Use .peek() instead of .forEach()

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

Upvotes: 1

KJTester
KJTester

Reputation: 407

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

Upvotes: 0

宏杰李
宏杰李

Reputation: 12178

 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-

Upvotes: 4

Grzegorz Piwowarek
Grzegorz Piwowarek

Reputation: 13883

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"))

Upvotes: 48

Misha
Misha

Reputation: 28183

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

Upvotes: 6

Jean-Baptiste Yun&#232;s
Jean-Baptiste Yun&#232;s

Reputation: 36441

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

Upvotes: 6

Eran
Eran

Reputation: 394146

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

Upvotes: 4

Eugene
Eugene

Reputation: 121078

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.

Upvotes: 22

Related Questions