Java Streams
Process collections declaratively with filter, map, reduce and collect.
What you’ll learn
- The stream pipeline
- filter() and map()
- reduce() and collect()
- Why streams are lazy
What is a stream?
A stream is a pipeline for processing a sequence of elements. Instead of telling Java how to loop, you describe what you want:
// Imperative
List<Integer> evens = new ArrayList<>();
for (int n : numbers) {
if (n % 2 == 0) evens.add(n * n);
}
// Declarative, with streams
List<Integer> evens = numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.toList();A stream does not store data and does not modify its source. It's a one-shot view over it.
The stream pipeline
Every pipeline has three parts:
- Source —
list.stream(),Arrays.stream(array),Stream.of(...). - Intermediate operations — return a new stream:
filter,map,sorted,distinct,limit. - Terminal operation — produces a result and ends the stream:
toList,collect,count,reduce,forEach.
filter()
Keeps elements matching a Predicate:
List<Employee> seniors = employees.stream()
.filter(e -> e.yearsOfExperience() >= 5)
.toList();map()
Transforms each element with a Function:
List<String> emails = employees.stream()
.map(Employee::email)
.toList();reduce()
Combines all elements into one value:
int totalSalary = employees.stream()
.map(Employee::salary)
.reduce(0, Integer::sum);
// For numbers, the specialised streams read better:
int total = employees.stream().mapToInt(Employee::salary).sum();collect()
collect with Collectors builds richer results — the most useful being groupingBy:
Map<String, List<Employee>> byDepartment = employees.stream()
.collect(Collectors.groupingBy(Employee::department));
Map<String, Double> avgSalary = employees.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.averagingInt(Employee::salary)));Streams are lazy
Intermediate operations don't run until a terminal operation asks for results. That lets Java fuse steps and stop early:
Optional<String> first = names.stream()
.peek(n -> System.out.println("checking " + n))
.filter(n -> n.startsWith("S"))
.findFirst();If the second name starts with "S", only two names are ever checked — not the whole list.
Common mistake
A stream can only be consumed once. Calling a second terminal operation on the same stream throws IllegalStateException.
⚡ Try it yourself
Given a List<Order> where each order has customer(), status() and amount(), compute the total amount per customer for orders with status PAID, sorted from highest to lowest.
Show a solution
orders.stream()
.filter(o -> o.status() == Status.PAID)
.collect(Collectors.groupingBy(Order::customer, Collectors.summingLong(Order::amount)))
.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.forEach(e -> System.out.println(e.getKey() + ": " + e.getValue()));Interview questions
QWhy are streams lazy?+
Laziness lets the stream process elements one at a time through the whole pipeline, fuse operations, and short-circuit (e.g. findFirst, limit, anyMatch) without touching the rest of the data. Nothing runs until a terminal operation is invoked.
QWhat is the difference between map() and flatMap()?+
map transforms each element into exactly one element. flatMap transforms each element into a stream and flattens those streams into one — for example turning a List<List<String>> into a single stream of strings.
Key takeaways
- Pipeline = source → intermediate ops → one terminal op.
filterselects,maptransforms,collect/reducecombine.- Streams are lazy and single-use.
Finished this lesson?
Mark it complete to move forward on your roadmap.