All topics
streams
intermediate

Stream Pipelines

Declarative pipelines for processing data — filter, map, reduce — that read like a description of what you want, not how to loop.

A stream is a way of describing what you want done to a collection, rather than writing the loop yourself.

A factory assembly line — raw items in, each station transforms or rejects them, packaging at the end. Nothing moves until the packaging line pulls.

Key Concepts

1
    list.stream().filter(u -> u.isActive()).map(User::getName).toList();
2
That reads as: take the list, keep the active ones, take their names, collect them.
3
Nothing happens until the last step. filter and map do not do any work when you call them. They build up a description. Only the final operation — toList, forEach, count — actually runs anything.
filtermaptoListforEachcount
4
A pipeline with no final operation does nothing at all, and nothing warns you.
5
Items flow through one at a time. This is the part people get wrong. It does not filter the whole list, then map the whole result.
6
Each item goes through every step before the next item starts. You can see it:
7
    Stream.of("a","b","c")
      .filter(s -> { print("filter " + s); return !s.equals("b"); })
      .map(s    -> { print("map " + s);    return s.toUpperCase(); })
      .findFirst();
8
    prints:  filter a
             map a          <- stops here; b and c are never touched
9
That is why findFirst is fast on a huge list: the stream stops pulling items as soon as it has an answer.
findFirst
10
Some steps have to see everything first. sorted cannot output anything until it has every item, because the first item alphabetically might be the last one you give it. distinct has to remember everything it has seen.
sorteddistinct
11
This makes the order of your steps matter:
12
    list.stream().sorted(cmp).limit(10)   // sorts a million items
    list.stream().limit(10).sorted(cmp)   // sorts ten
13
Both are valid; they answer different questions. The first gives the ten smallest overall. The second sorts the first ten it happens to find. Know which one you meant.
14
A stream can only be used once. After the final operation it is spent. Using it again throws IllegalStateException. If you need two results, either collect once and reuse the collection, or build the stream twice.
IllegalStateException
15
Do not change things from inside a stream. Adding to a list from forEach works when running normally and breaks when the stream runs in parallel. Use collect to build a result instead.
forEachcollect
16
peek is for debugging only. It looks like a way to log each item, and Java does not promise to run it for every item — it may skip it when it can prove the result does not depend on it. Logging with peek is therefore unreliable.
peek
17
What the interviewer is probing.1. "When does a stream run?" Probing: laziness. Stalls: "When you call it." Moves up: only at the final operation; without one, nothing happens.
18
2. "Filter then map, or map then filter?" Probing: whether you know items flow one at a time. Stalls: "Same result." Moves up: same result, less work — filtering first means map runs on fewer items.
map
19
3. "Why is sorted().limit(10) more expensive?" Probing: steps that buffer. Stalls: "They are the same." Moves up: sorted must see everything before emitting anything.
sorted().limit(10)sorted
20
4. "Can you reuse a stream?" Probing: the one-shot rule. Stalls: "Yes." Moves up: no, it throws; keep the collection instead.
21
5. "What is peek for?" Probing: the trap. Stalls: "Logging in production." Moves up: debugging only, and not guaranteed to run for every item.
peek
22
  Operation classes
23
  Intermediate, stateless   filter, map, flatMap, peek, mapToInt
                            -> element passes straight through
  Intermediate, stateful    sorted, distinct, limit, skip
                            -> must buffer, sometimes the whole
                               stream
  Terminal, short-circuit   findFirst, findAny, anyMatch, allMatch,
                            noneMatch, limit
                            -> can stop the source early
  Terminal, consuming       collect, reduce, forEach, count, sum,
                            toArray
24
  Element-at-a-time, demonstrated
25
    Stream.of("a","b","c")
          .filter(s -> { print("filter " + s); return !s.equals("b"); })
          .map(s -> { print("map " + s); return s.toUpperCase(); })
          .findFirst();
26
    prints:  filter a
             map a           <- stops here; b and c never touched
27
  Ordering matters
28
    // sorts 1,000,000 elements, keeps 10
    list.stream().sorted(cmp).limit(10)
29
    // sorts 10 -- but only equivalent if you want the first 10
    //           in encounter order, not the smallest 10
    list.stream().limit(10).sorted(cmp)
30
    // for "smallest 10 overall", sorted().limit() is correct and
    // the cost is inherent -- say so rather than misapplying the
    // optimisation.
31
  Boxing
    IntStream.range(0,1_000_000).sum()          no allocation
    Stream.iterate(0,i->i+1).limit(1_000_000)
          .mapToInt(Integer::intValue).sum()    1M Integer objects