filter, map & forEach
filter, map & forEach
Every stream pipeline is built from three kinds of operation: a source (covered in lesson 2), zero or more intermediate operations that transform the stream, and exactly one terminal operation that triggers evaluation and produces a result. This lesson covers the three operations you will use in virtually every pipeline: filter, map, and forEach.
Lazy evaluation — why it matters
Intermediate operations such as filter and map do nothing until a terminal operation is invoked. The stream is a description of what to do, not a loop that is already running. This laziness allows the JVM to fuse operations and avoid unnecessary work — for example, once filter finds the first match when used with findFirst(), the rest of the source is never touched.
filter and map return a new Stream — they are intermediate. forEach returns void — it is terminal. A stream can only be consumed once; after the terminal operation fires, the stream is exhausted.
filter — keeping elements that match a predicate
filter(Predicate<T> predicate) is an intermediate operation that passes only those elements for which the predicate returns true.
The predicate is any expression that returns a boolean. You can compose predicates with Predicate.and(), Predicate.or(), and Predicate.negate() to keep the lambda readable:
map — transforming every element
map(Function<T, R> mapper) is an intermediate operation that applies a function to every element, producing a Stream<R>. The stream type can change — mapping Stream<String> through String::length produces Stream<Integer>.
A more common real-world pattern is extracting a field from an object:
Product::name, String::toUpperCase) are the idiomatic shorthand for a one-argument lambda that calls a single method. Prefer them when the intent is obvious.
Combining filter and map
The real power emerges when you chain the two. Operations execute element-by-element through the pipeline — element 1 passes through filter then map, then element 2, and so on. There is no intermediate list:
forEach — consuming every element
forEach(Consumer<T> action) is a terminal operation that executes the action for every element remaining in the stream. It returns void and is typically used for side effects — printing, logging, or writing to an external system.
forEach, and adding elements inside the lambda, stop. That is exactly what collect() is for (covered in lesson 4). forEach is for side effects only — mutating shared state inside a stream lambda breaks the stream model and causes bugs with parallel streams.
forEachOrdered — when order matters
For sequential streams the order is deterministic. For parallel streams, forEach does not guarantee encounter order. Use forEachOrdered when order is required and you are also using parallel():
Putting it all together
Here is a self-contained example that ties all three operations into a realistic scenario:
Summary
filter(predicate)— intermediate; keeps elements where the predicate istrue.map(function)— intermediate; transforms every element, potentially changing the type.forEach(consumer)— terminal; executes a side-effect action and exhausts the stream.- Intermediate operations are lazy: they execute only when a terminal operation is called.
- Reserve
forEachfor side effects; usecollect()when you need a result.