skip to content
everything.functional.streamOperations

stream operations

map, filter, flatMap, sorted, distinct, limit, takeWhile.

chapter
chapter 07.05
reading time
6 min read

intermediate operations come in two kinds. stateless ones (filter, map, flatMap) judge each element on its own. stateful ones (distinct, sorted, limit, skip) have to remember something about the elements they've already seen.

some are also short-circuiting (limit, takeWhile): they can stop the pipeline before the source runs dry. and one of them, sorted, is a wall: nothing gets past it until everything has arrived.

the picture

you know how filter and sorted sit next to each other in a pipeline like they're the same kind of thing? they're not. filter can decide about element 3 the moment it sees it. sorted can't output anything until it has seen the last element, because the last one might be the smallest.

think of an airport. passport control is stateless: each traveller is checked on their own and walks on. the boarding queue sorted by zone is stateful: nobody boards until the queue has formed. same airport, very different flow.

op =
Stream.of(List.of(1, 2), List.of(3), List.of(), List.of(4, 5, 6)) .flatMap(List::stream) .toList()
inout123empty456123456
stateless

each element becomes a whole stream, and those streams are poured into one. the empty list contributes nothing. plain map(List::stream) would give you a Stream<Stream<Integer>> instead: 4 elements, each a stream.

pick an operation. the top row goes in, the bottom row comes out, and each line shows which input an output came from. red inputs were pulled and dropped, dashed ones were never pulled at all. the purple band is the state a stateful operation has to keep. every output is the real one from jdk 25.

the cheat sheet

operationwhat it doeskind
filter(p)keeps elements where p is truestateless
map(f)replaces each element with f(element)stateless
flatMap(f)replaces each element with all the elements of the stream f returnsstateless
mapMulti(f)like flatMap, but you push results into a consumer instead of building a stream (java 16)stateless
peek(c)runs c on each element as it passes. for debuggingstateless
distinct()drops repeats, by equals/hashCodestateful
sorted()sorts. natural order, or sorted(comparator)stateful, a barrier
limit(n) / skip(n)keeps the first n / drops the first nstateful. limit short-circuits
takeWhile(p) / dropWhile(p)keeps / drops the leading run where p holds (java 9)stateful. takeWhile short-circuits

flatMap: one to many

map is one in, one out. flatMap is one in, any number out: each element becomes a little stream of its own, and those streams are poured into the main one. the classic use is "a list of things that each have a list":

Main.java
import java.util.*;
 
record Order(String customer, List<String> items) {}
 
void main() {
    List<Order> orders = List.of(
            new Order("ada", List.of("tea", "scone")),
            new Order("bob", List.of()),
            new Order("cy", List.of("tea", "cake", "tea")));
 
    IO.println(orders.stream()
            .flatMap(o -> o.items().stream())     // 3 orders in, 5 items out
            .toList());
 
    IO.println(orders.stream()
            .flatMap(o -> o.items().stream())
            .distinct()        // stateful: remembers what it's seen
            .sorted()          // stateful: waits for everything
            .toList());
 
    IO.println(orders.stream()
            // flatMap without building the inner streams
            .<String>mapMulti((o, out) -> {
                for (String item : o.items()) {
                    out.accept(o.customer() + ":" + item);
                }
            })
            .toList());
}
output
[tea, scone, tea, cake, tea]
[cake, scone, tea]
[ada:tea, ada:scone, cy:tea, cy:cake, cy:tea]

bob's empty order just disappears: an empty inner stream contributes nothing. mapMulti does the same job by handing you a Consumer to push results into. it's handy when each element produces zero or one or two results, where building a tiny stream for each would be wasteful.

sorted is a barrier

every stateless operation passes elements along one at a time, like the previous chapter showed. sorted breaks that. it swallows everything, sorts, and only then lets the elements continue:

Main.java
import java.util.stream.*;
 
void main() {
    Stream.of(3, 1, 2)
            .peek(n -> IO.println("before " + n))
            .sorted()
            .forEach(n -> IO.println("after  " + n));
}
output
before 3
before 1
before 2
after  1
after  2
after  3

swap sorted() for map(n -> n) and the output interleaves: before 3, after 3, before 1, after 1... that's the difference between a pipe and a wall.

takeWhile isn't filter

predict the output
java
List.of(3, 1, 4, 1, 5, 9, 2, 6).stream()
        .takeWhile(n -> n < 5)
        .toList()

peek is for looking, not doing

peek runs a side effect as elements pass. it's great for the print statements in this chapter. it's a trap for anything that matters, because the JDK is allowed to skip it:

Main.java
import java.util.stream.*;
 
void main() {
    long a = Stream.of(1, 2, 3)
            .peek(x -> IO.println("peek " + x))
            .count();
    IO.println(a);
 
    long b = Stream.of(1, 2, 3)
            .filter(x -> { IO.println("filter " + x); return true; })
            .count();
    IO.println(b);
}
output
3
filter 1
filter 2
filter 3
3

no "peek" lines at all. the source knows it has 3 elements and nothing in the pipeline can change that, so since java 9 count() just returns 3 without running anything. the filter version has to run, because a filter might drop things. if you need a side effect, put it in forEach, or better, collect a result and act on that.

keylines
  • stateless ops look at one element at a time. stateful ops remember something.
  • flatMap is one-to-many. empty inner streams just vanish.
  • sorted is a barrier: everything in before anything out. never after an infinite source.
  • takeWhile stops at the first failure. filter checks everyone.
  • peek can be skipped (hello, count()). don't put real work in it.