Skip to content
PickNTech

Java Streams

Process collections declaratively with filter, map, reduce and collect.

Intermediate20 minUpdated 23 Sept 2026

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→filter()→map()→collect()
A source, zero or more intermediate operations, and exactly one terminal operation.
  1. Source — list.stream(), Arrays.stream(array), Stream.of(...).
  2. Intermediate operations — return a new stream: filter, map, sorted, distinct, limit.
  3. 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.
  • filter selects, map transforms, collect/reduce combine.
  • Streams are lazy and single-use.

Finished this lesson?

Mark it complete to move forward on your roadmap.