The Stream API
The pipeline model, map/filter/reduce, collectors, and when not to go parallel.
Deutsche Übersetzung in Arbeit
Diese Lektion ist noch nicht ins Deutsche übersetzt und wird daher auf Englisch angezeigt. Der Rest der Seite ist vollständig lokalisiert.
Auf dieser Seite
The Stream API is modern Java's most loved feature. It lets you process collections of data as a pipeline of operations - filter, transform, aggregate - in clear, declarative steps. Once it clicks, you'll never want to write a manual loop again.
The pipeline model
A stream pipeline has three parts:
- A source - where the data comes from (a list, a file, a range).
- Intermediate operations - transformations like
filterandmap(lazy; they build up the pipeline). - A terminal operation - like
collectorcountthat triggers processing and produces a result.
Raw materials (your data) enter one end. Each station on the line does one job -
inspect and reject (filter), reshape (map), count or box up (collect). The
line only runs when someone at the end asks for the finished product (the terminal
operation). Nothing happens until then.
List<String> names = List.of("Ada", "Grace", "Alan", "Bob");
List<String> result = names.stream() // source
.filter(n -> n.length() > 3) // intermediate
.map(String::toUpperCase) // intermediate
.sorted() // intermediate
.toList(); // terminal
System.out.println(result); // [ALAN, GRACE]Common operations
filter - keep elements matching a condition:
numbers.stream().filter(n -> n % 2 == 0) // even numbers onlymap - transform each element:
words.stream().map(String::length) // each word -> its lengthreduce - combine all elements into one value:
int sum = numbers.stream().reduce(0, Integer::sum); // add them all upcollect / toList - gather results into a collection:
Set<String> unique = words.stream().collect(Collectors.toSet());Collectors: powerful aggregation
Collectors do sophisticated grouping and summarizing:
// Group people by their city
Map<String, List<Person>> byCity = people.stream()
.collect(Collectors.groupingBy(Person::city));
// Join names into one string
String line = names.stream().collect(Collectors.joining(", "));
// Count, average, sum
double avgAge = people.stream()
.collect(Collectors.averagingInt(Person::age));Laziness: why it's efficient
Intermediate operations are lazy - they do nothing until a terminal operation runs. This lets streams skip unnecessary work:
Optional<String> first = names.stream()
.filter(n -> n.startsWith("A"))
.findFirst(); // stops as soon as the first match is foundStreams don't modify the source
A stream never changes the original collection - it produces a new result. This fits the functional, immutable style: your source data stays intact, and you get a fresh output.
Parallel streams - and when NOT to use them
Adding .parallel() splits work across CPU cores:
long count = hugeList.parallelStream()
.filter(expensive::test)
.count();Parallel is not always faster
Parallel streams add coordination overhead and only help with large datasets
and CPU-heavy, independent work. For small collections or I/O-bound tasks,
they're often slower - and they can cause subtle bugs if your operations aren't
thread-safe. Measure before reaching for .parallel(); default to sequential.
A complete example
record Order(String product, String category, double amount) {}
Map<String, Double> revenueByCategory = orders.stream()
.filter(o -> o.amount() > 0)
.collect(Collectors.groupingBy(
Order::category,
Collectors.summingDouble(Order::amount)));In a handful of readable lines, we filtered, grouped, and summed - code that would take a nested loop and several helper variables the imperative way.
Quick check
In a stream pipeline, when do the intermediate operations (filter, map) actually run?
Key takeaways
- A stream pipeline = source -> intermediate operations -> terminal operation.
- filter keeps matching elements; map transforms them; reduce combines them; collect gathers results.
- Collectors enable grouping (groupingBy), joining, counting, and summarizing.
- Intermediate operations are lazy - nothing runs until a terminal operation is called.
- Streams never modify their source; they produce new results.
- Use parallel streams only for large, CPU-bound, independent work - and measure first.
Next, we peek behind the curtain at how frameworks work their magic: annotations and reflection.