🌊 Streams API · Intermediate

What a stream is

A pipeline over data: source → intermediate ops → terminal op; not a data structure.

🧩 The mysteryYou 'uppercase' a list with a stream, print the list… and it's still lowercase. Did Java ignore you, or did you misunderstand what a stream is?

A conveyor belt, not a box

Picture a factory: the List is the shelf that holds the boxes, and a stream is the conveyor belt that carries them past workers. The belt holds nothing of its own. In the same way, a stream stores no elements — it describes a computation that pulls items from a source.

Every pipeline has three parts

Source → zero or more intermediate ops → exactly one terminal op. The source feeds the belt, intermediate ops reshape what flows, and the terminal op produces the final result.

long n = names.stream()        // source
    .filter(s -> s.length() > 3) // middle
    .count();                    // terminal

Intermediate vs terminal

Intermediate ops like filter, map and sorted return a new Stream, so you can keep chaining. Terminal ops like count, toList, collect and forEach end the pipeline and produce a result (or a side effect). Tip: if it returns a Stream, it's intermediate.

🔮 Predict it

Did the source change?

What do you think this prints?

List<Integer> nums =
    new ArrayList<>(List.of(1, 2, 3));
List<Integer> doubled = nums.stream()
    .map(n -> n * 2)
    .toList();
System.out.println(nums + " " + doubled);
  1. [2, 4, 6] [2, 4, 6]
  2. [1, 2, 3] [2, 4, 6]
  3. [1, 2, 3] [1, 2, 3]
Show the answer

[1, 2, 3] [2, 4, 6] — map builds new values in a new list. The stream reads nums but never modifies its source.

Expecting the stream to edit the list

✗ Does nothing useful
names.stream()
    .map(String::toUpperCase);
System.out.println(names); // unchanged

No terminal op, and even with one, the source list is never changed.

✓ Keep the result
List<String> upper = names.stream()
    .map(String::toUpperCase)
    .toList();

The terminal op toList() hands you a brand-new list with the results.

💼 In the real world

Why teams love streams

Code reviews in modern Java shops are full of pipelines like orders.stream().filter(...).map(...).toList(). Because the source is never modified, you can safely stream over shared data without corrupting it — and the three-part shape makes intent easy to read.

Key takeaways

  1. Shape: source → intermediate ops → one terminal op
  2. Intermediate ops (filter, map…) return a new Stream
  3. Terminal ops (count, toList, forEach…) produce a result
  4. The source collection is never modified by the stream

💡 A stream is like a factory conveyor belt: items flow from a loading dock, through stations, into a final packing box.

🤯 Did you know?

The java.util.stream API has nothing to do with java.io.InputStream — two unrelated APIs that just happen to share the word "stream".

Practice questions

Which order describes every stream pipeline?

  1. Terminal op → intermediate ops → source
  2. Source → intermediate ops → terminal op
  3. Source → terminal op → intermediate ops
  4. Intermediate ops → source → terminal op
Check your answer

Source → intermediate ops → terminal op. You start from a source (like list.stream()), chain zero or more intermediate operations, and finish with exactly one terminal operation that produces the result.

What does this print?

List<String> names =
    new ArrayList<>(List.of("ann", "bob"));
List<String> upper = names.stream()
    .map(String::toUpperCase)
    .toList();
System.out.println(names);
System.out.println(upper);
  1. [ANN, BOB] [ANN, BOB]
  2. [ann, bob] [ANN, BOB]
  3. [ann, bob] [ann, bob]
  4. [ANN, BOB] [ann, bob]
Check your answer

[ann, bob] [ANN, BOB]. map creates new values in a new result list. The original names list is untouched, because a stream never modifies its source.

Next: lists aren't the only doorway into a stream. What happens if you hand Stream.of an int[]?