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.
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.
the cheat sheet
| operation | what it does | kind |
|---|---|---|
filter(p) | keeps elements where p is true | stateless |
map(f) | replaces each element with f(element) | stateless |
flatMap(f) | replaces each element with all the elements of the stream f returns | stateless |
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 debugging | stateless |
distinct() | drops repeats, by equals/hashCode | stateful |
sorted() | sorts. natural order, or sorted(comparator) | stateful, a barrier |
limit(n) / skip(n) | keeps the first n / drops the first n | stateful. 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":
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());
}[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:
import java.util.stream.*;
void main() {
Stream.of(3, 1, 2)
.peek(n -> IO.println("before " + n))
.sorted()
.forEach(n -> IO.println("after " + n));
}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
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:
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);
}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.
- stateless ops look at one element at a time. stateful ops remember something.
flatMapis one-to-many. empty inner streams just vanish.sortedis a barrier: everything in before anything out. never after an infinite source.takeWhilestops at the first failure.filterchecks everyone.peekcan be skipped (hello,count()). don't put real work in it.