What a stream is
A pipeline over data: source → intermediate ops → terminal op; not a data structure.
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(); // terminalIntermediate 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.
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);[2, 4, 6] [2, 4, 6][1, 2, 3] [2, 4, 6][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
names.stream()
.map(String::toUpperCase);
System.out.println(names); // unchangedNo terminal op, and even with one, the source list is never changed.
List<String> upper = names.stream()
.map(String::toUpperCase)
.toList();The terminal op toList() hands you a brand-new list with the results.
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
- Shape: source → intermediate ops → one terminal op
- Intermediate ops (filter, map…) return a new Stream
- Terminal ops (count, toList, forEach…) produce a result
- 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.
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?
- Terminal op → intermediate ops → source
- Source → intermediate ops → terminal op
- Source → terminal op → intermediate ops
- 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);- [ANN, BOB] [ANN, BOB]
- [ann, bob] [ANN, BOB]
- [ann, bob] [ann, bob]
- [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.