Chapter 8 of 18
08 — Streams
filter, map, and reduce — the functional way to process collections.
Kata: transform and reduce lists functionally — filter, map, distinct, sum.
New ideas: the Stream API, method references, and the three verbs of functional collection
processing.
The Stream API
The Stream API is a pipeline of operations — source → filters/maps → reduce — that reads
like the intent rather than the mechanism.
Every stream exercise is written test-first, so the API is self-explanatory:
@Test
void evensFiltersTheList() {
assertEquals(List.of(2, 4, 6), Numbers.evens(List.of(1, 2, 3, 4, 5, 6)));
}
RED → implement:
public static List<Integer> evens(List<Integer> numbers) {
return numbers.stream()
.filter(n -> n % 2 == 0)
.toList();
}
GREEN. The three stream verbs, all test-first:
| Operation | Mindset | Java |
|---|---|---|
filter |
keep | stream().filter(predicate) |
map |
transform each | stream().map(function) |
| terminal | produce a result | .toList(), .count(), .sum() |
Step 2 — map and reduce
@Test
void sumOfSquaresReducesToASingleValue() {
assertEquals(14, Numbers.sumOfSquares(List.of(1, 2, 3)));
}
public static int sumOfSquares(List<Integer> numbers) {
return numbers.stream()
.mapToInt(n -> n * n) // maps AND unboxes to int stream
.sum(); // reduces to one number
}
.mapToInt is the specialized stream avoiding boxing. And sum is a reduce in disguise.
Step 3 — distinct collects
public static long countDistinct(List<Integer> numbers) {
return numbers.stream().distinct().count();
}
Java-specific notes
- Method references:
stream().map(s -> s.length())can be writtenstream().map(String::length). Use them where they read cleanly. .toList()(Java 16+) returns an unmodifiable list;.collect(Collectors.toList())is the older form. Prefer.toList().- Laziness: streams are lazy (nothing runs until a terminal op).
filterfirst meansmapruns on fewer elements — performance matters as inputs grow. Tests won’t show you this; it’s a design habit. - Parallel:
stream().parallel()...exists, but don’t reach for concurrency until a test proves you need it — see the concurrency chapter for how to do that properly.
Why this matters for interviews
Live-coding a correct, readable pipeline (filter/map/reduce) in 30 seconds is a strong signal.
Combine it with records (ch. 05) and Optional (ch. 09) and you answer most “write me a function
that processes X” questions with one clean chain.
Run it
gradle test --tests "streams.NumbersTest"
Key takeaway: streams replace loops the way loops replaced goto — the test describes the what, the pipeline describes the how. If a pipeline starts feeling clever, your tests will (out)date you about it.
Next: 09 — optionals — missing values without null.