← The curriculum
§ 4.01 · Modern Java

Streams, lambdas, and functional pipelines

Transform collections declaratively. map, filter, reduce, collect — and when to fall back to a for-loop.

Intermediate · 35 minute read
Try it yourself
Live Java · editable

Runs on a sandboxed JVM via the public Piston API. Edit the code and hit Run.

§ 4.01.01

Lambdas and method references

A lambda is shorthand for an anonymous implementation of a functional interface — an interface with a single abstract method. x -> x * 2, (a, b) -> a + b, and String::length are all lambdas. They capture variables from the enclosing scope but only if those variables are effectively final.

§ 4.01.02

The stream pipeline

A stream is a description of a computation over a source. It has three parts: a source (collection.stream(), Stream.of(...), IntStream.range(...)), zero or more intermediate operations (map, filter, sorted, distinct), and exactly one terminal operation (collect, forEach, count, reduce). Nothing runs until the terminal operation is called — streams are lazy.

§ 4.01.03

map, filter, reduce

map transforms each element. filter keeps only elements matching a predicate. reduce folds the stream to a single value with a binary operator. Together they cover most transformations you used to write with for-loops.

Example
Live Java · editable

Runs on a sandboxed JVM via the public Piston API. Edit the code and hit Run.

§ 4.01.04

Collectors

collect(Collectors.toList()) — or the shorter .toList() in Java 16+ — is the workhorse terminal. Collectors.groupingBy, partitioningBy, toMap, and joining handle almost every aggregation. toMap throws on duplicate keys unless you pass a merge function.

§ 4.01.05

Primitive streams

IntStream, LongStream, and DoubleStream avoid boxing. Use mapToInt / mapToObj to move between object and primitive streams. IntStream.range(0, n) is the idiomatic replacement for a counted for-loop when you're already in a stream chain.

§ 4.01.06

When not to use streams

For simple iteration with side effects, a for-each loop is clearer and often faster. Streams shine for multi-step transformations, grouping, and parallel processing. Never mutate external state from inside a stream — the contract is functional, and parallel streams will corrupt data if you break it.