Course outline · 0% complete

0/29 lessons0%

Course overview →

Streams: filter, map, collect

lesson 9-2 · ~7 min · 25/29

The stream pipeline

Most collection work is the same three verbs in different orders: keep some elements, transform them, gather a result.

Written as loops, those verbs smear across mutable helper variables. Written as a stream, each verb is one named stage, and a reviewer reads the intent line by line. Streams are the house style for this in modern Java codebases, so you will read them daily even before you write them.

A stream processes a collection in stages, each stage taking a lambda. It is Java's answer to Python's list comprehensions:

[n * n for n in nums if n % 2 == 0]
nums.stream()
    .filter(n -> n % 2 == 0)
    .map(n -> n * n)
    .collect(Collectors.toList());

The stages are:

StageRole
stream()opens the pipeline
filter(predicate)keeps elements that pass the test
map(function)transforms each element
a terminal operationends the pipeline and produces the result

collect(Collectors.toList()) builds a list, count() counts, and mapToInt(...).sum() totals.

Nothing runs until the terminal operation arrives, and the source collection is never modified, which is the guarantee that makes a pipeline safe to read as a description rather than as a sequence of mutations.

source[1..6]filterkeep evensmapn → n²collect
Elements flow through the pipeline one stage at a time: filtered, transformed, then gathered by the terminal collect.

One list, three pipelines

A collected list, a sum, and a count, all from the same source.

import java.util.List;
import java.util.stream.Collectors;

public class Main {
  public static void main(String[] args) {
    List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);

    List<Integer> evenSquares = nums.stream()
        .filter(n -> n % 2 == 0)
        .map(n -> n * n)
        .collect(Collectors.toList());
    System.out.println(evenSquares);

    int total = nums.stream().mapToInt(n -> n).sum();
    System.out.println(total);

    long bigOnes = nums.stream().filter(n -> n > 3).count();
    System.out.println(bigOnes);
  }
}

Output

[4, 16, 36]
21
3

The filter keeps 2, 4, and 6, and the map squares each survivor, so the order of the two stages matters. Squaring first and filtering after would give the same answer here but not in general.

nums is untouched afterwards, which is why three separate stream() calls on it all see the original six numbers. mapToInt is the bridge to primitive arithmetic, since a stream of Integer has no sum of its own.

More stages and endings

A few more pieces cover most day-to-day pipelines. Mid-pipeline stages include distinct() to drop duplicates, sorted() to order elements, and limit(n) to keep the first n.

Terminal operations besides collect:

TerminalProduces
count()how many elements survived, as a long
anyMatch(predicate)true as soon as one element passes
mapToInt(n -> n).sum()a total, since int math needs the mapToInt bridge
toList()a List, the modern shorthand for collect(Collectors.toList())

One rule keeps pipelines honest: a stream never modifies its source, and each stream can be consumed by exactly one terminal operation, so build a fresh stream() for each question you ask.

anyMatch also short-circuits, stopping at the first element that passes rather than examining the rest. Combined with laziness, that means a filter followed by anyMatch can finish after inspecting one element.

Four questions about one list

A count, a cleaned and sorted copy, a yes-or-no, and a sum.

import java.util.List;

public class Main {
  public static void main(String[] args) {
    List<Integer> nums = List.of(4, 1, 7, 4, 2, 7, 9);

    long bigCount = nums.stream().filter(n -> n > 3).count();
    System.out.println("greater than 3: " + bigCount);

    List<Integer> cleaned = nums.stream().distinct().sorted().toList();
    System.out.println(cleaned);

    boolean anyEven = nums.stream().anyMatch(n -> n % 2 == 0);
    System.out.println("any even: " + anyEven);

    int total = nums.stream().mapToInt(n -> n).sum();
    System.out.println("sum: " + total);
  }
}

Output

greater than 3: 5
[1, 2, 4, 7, 9]
any even: true
sum: 34

The five values above 3 are 4, 7, 4, 7, and 9, counting the repeats, because filter never removes duplicates on its own. distinct().sorted() produces the five unique values in order without touching nums.

Each question opens its own stream, which is required rather than stylistic. Reusing a consumed stream throws an IllegalStateException at runtime.

What triggers the work

The terminal operation is what actually runs a pipeline, whether that is collect(), count(), or another ending.

Intermediate operations such as filter and map are lazy, meaning they only describe the pipeline. The terminal operation pulls elements through all the stages and produces the final value.

nums.stream().filter(n -> n > 3);   // legal, and does nothing at all
nums.stream().filter(n -> n > 3).count();   // now the filter runs

The first line compiles, produces a stream object, and never evaluates the lambda even once.

Laziness is not just an implementation detail. It lets a pipeline over a million elements stop early on anyMatch or limit(10), and it means each element flows through every stage in turn rather than the whole collection being copied at each step.

Filtering words two ways

One pipeline builds a transformed list, the other answers a count.

import java.util.List;
import java.util.stream.Collectors;

public class Main {
  public static void main(String[] args) {
    List<String> words = List.of("stream", "java", "lambda", "api", "code");
    List<String> shortUpper = words.stream()
        .filter(w -> w.length() <= 4)
        .map(w -> w.toUpperCase())
        .collect(Collectors.toList());
    System.out.println(shortUpper);

    long longCount = words.stream().filter(w -> w.length() > 4).count();
    System.out.println(longCount);
  }
}

Output

[JAVA, API, CODE]
2

The first pipeline keeps words of length 4 or less, then uppercases each survivor, and collect gathers them in the original relative order. The two long words are stream and lambda, so the count is 2.

count() returns a long rather than an int, since a stream can in principle be longer than an int can hold, so store it in a long or print it directly.