Pipelines Instead of Loops
A stream is a pipeline for processing a sequence of values. You start from a source - usually a collection - chain together operations like "keep only these", "transform each one", "sort them", and finish by collecting the result. Instead of writing a loop with a temporary list and an if inside, you describe what you want and let the stream do the walking.
Now that you have lambdas, streams click into place: every operation takes a lambda (or method reference) describing the work for one element.
Read it top to bottom: take the names, keep the long ones, uppercase them, collect to a list. The original names list is never modified - a stream produces a new result and leaves the source alone.
The Anatomy of a Stream
Every pipeline has three parts:
- A source -
list.stream(),Arrays.stream(array), orStream.of(a, b, c). - Zero or more intermediate operations -
filter,map,sorted,distinct,limit. Each returns another stream, so you can chain them. - Exactly one terminal operation -
collect,count,forEach,reduce,findFirst. This kicks off the work and produces a result (or a side effect).
The split matters because of one rule that surprises everyone at first.
Intermediate Operations Are Lazy
Intermediate operations do nothing on their own. They just record what should happen. The pipeline only runs when a terminal operation asks for a result.
Run it: the map lambda never executes. Add a terminal operation and the whole chain springs to life:
This laziness is not a quirk to fight - it lets the stream fuse operations and skip work it does not need. But it means a pipeline without a terminal operation is a no-op, which is a common "why is nothing happening?" bug.
filter and map: Keep and Transform
These two carry most of the weight. filter takes a Predicate (a lambda returning boolean) and keeps elements that pass. map takes a Function and replaces each element with the result of applying it.
Note that map can change the element type: here a stream of String becomes a stream of Integer. The String::length is a method reference - shorthand for the lambda w -> w.length().
Terminal Operations Produce a Result
Once you have shaped the stream, a terminal operation turns it into something concrete:
Common terminals: collect (gather into a list/set/map), count, forEach, anyMatch / allMatch / noneMatch, findFirst, min / max, and reduce. After a terminal runs, the stream is consumed - you cannot reuse it. Call list.stream() again for a fresh pipeline.
Collecting Results
collect with Collectors is the workhorse for building a result. The most common is Collectors.toList():
Collectors also gives you toSet(), joining(...), groupingBy(...), and counting(). On Java 16 and later you can replace collect(Collectors.toList()) with the shorter .toList() (it returns an unmodifiable list):
List<String> result = names.stream().map(String::toUpperCase).toList();
sorted, distinct, and limit
These intermediate operations reshape the stream before you collect:
sorted() with no argument uses natural ordering; pass a Comparator (here a lambda) for custom order. Comparator.reverseOrder() is the cleaner way to sort descending.
reduce: Fold a Stream to One Value
When you need to combine all elements into a single result - a sum, a product, the longest string - reduce is the general tool. You give it a starting value and a function that merges two values:
For plain sums and averages, the specialized streams are clearer: nums.stream().mapToInt(Integer::intValue).sum(). Reach for reduce when there is no ready-made aggregator.
Streams Do Not Replace Every Loop
Streams shine when you are transforming a collection into a result. They are not automatically faster than a loop, and they are awkward when you need to mutate outside state or break out early in fiddly ways. A good rule: if the pipeline reads as one clear sentence, use a stream; if you are reaching for a shared counter or an index, a plain for loop is honest and fine.
Also remember a stream is single-use. Operating on it twice throws an exception:
Stream<String> s = names.stream();
s.forEach(System.out::println);
s.count(); // IllegalStateException: stream has already been operated upon
Next: Optional
Several stream terminals - findFirst, min, max, reduce without an identity - might find nothing, so they do not return a bare value. They return an Optional, Java's container for "maybe a value, maybe nothing", which finally gives you a clean alternative to returning null. That is the next page.
Frequently Asked Questions
What is a stream in Java?
A stream is a pipeline for processing a sequence of elements - usually sourced from a collection - through a chain of operations like filter, map, and sorted, ending in a terminal operation like collect or count. It does not store data and does not modify the source; it describes a computation. You build one with list.stream() and read the chain top to bottom like a recipe.
How do you convert a stream back to a list in Java?
End the pipeline with a terminal collector: list.stream().filter(...).collect(Collectors.toList()). On Java 16+ you can use the shorter .toList(), which returns an unmodifiable list. Without a terminal operation nothing runs at all, because intermediate operations like filter and map are lazy.
When should you use a stream instead of a for loop?
Reach for a stream when you are transforming or filtering a collection into a result - the pipeline reads as one clear sentence ("take the names, keep the long ones, uppercase them, collect to a list"). Stick with a plain for loop when you need to mutate external state, break out early in complex ways, or the logic is simpler as imperative steps. Streams are about clarity, not raw speed.