Stream Operations
Stream operations fall into two categories: intermediate operations, which transform a stream into another stream and are lazy, and terminal operations, which consume the stream and actually produce a result. Building a useful pipeline usually means chaining a few intermediate operations and finishing with exactly one terminal operation.
Intermediate operations
Each of these returns a new stream, so they can be chained together fluently. None of them run any actual work until a terminal operation is called.
Operation | What it does |
|---|---|
| Keeps only elements matching the predicate |
| Transforms each element into a new value |
| Sorts elements by natural order or a given comparator |
| Removes duplicate elements (using |
| Truncates the stream to at most n elements |
| Discards the first n elements |
Terminal operations
A terminal operation triggers the entire pipeline to actually run and produces a concrete result — a collection, a value, or nothing at all in the case of .forEach(). After a terminal operation runs, the stream is consumed and cannot be reused.
Operation | What it does |
|---|---|
| Gathers elements into a |
| Performs an action on each element |
| Returns the number of elements as a |
| Combines all elements into a single value |
| Short-circuiting boolean checks against a predicate |
| Returns an |
A full worked example
The example below chains several intermediate operations — filtering, mapping, and sorting — before finishing with a .collect() terminal operation.
Filter, map, sort, and collect
record Employee(String name, String department, double salary) {}
List<Employee> employees = List.of(
new Employee("Alice", "Engineering", 95000),
new Employee("Bob", "Sales", 62000),
new Employee("Charlie", "Engineering", 110000),
new Employee("Dave", "Sales", 58000)
);
List<String> topEngineerNames = employees.stream()
.filter(e -> e.department().equals("Engineering")) // keep only Engineering
.filter(e -> e.salary() > 90000) // keep high earners
.map(Employee::name) // Employee -> String
.sorted() // alphabetical order
.collect(Collectors.toList()); // gather into a List
System.out.println(topEngineerNames); // [Alice, Charlie]Each .filter() and .map() call returns a new stream, so they read top-to-bottom as a sequence of transformation steps, and only .collect() at the end actually executes the pipeline and produces the result.
A few terminal operations in isolation
List<Integer> numbers = List.of(4, 9, 15, 20, 2, 8); long countAboveTen = numbers.stream().filter(n -> n > 10).count(); // 2 boolean anyNegative = numbers.stream().anyMatch(n -> n < 0); // false Optional<Integer> firstEven = numbers.stream().filter(n -> n % 2 == 0).findFirst(); System.out.println(firstEven.get()); // 4 int sum = numbers.stream().reduce(0, Integer::sum); // 58
Intermediate operations (
filter,map,sorted,distinct,limit,skip) return a new stream and can be chained.Terminal operations (
collect,forEach,count,reduce, the*Matchmethods,findFirst) trigger execution and consume the stream.A pipeline typically chains several intermediate operations and ends in exactly one terminal operation.
.collect()is the most common terminal operation, usually paired with a factory method fromCollectors.