Learning goals
By the end of this chapter you should be able to:
- Build a stream pipeline: source → intermediate ops → terminal op
- Use
map,filter,flatMap,sorted,distinct,limit - Collect with
toList,Collectors.groupingBy,Collectors.toMap - Know when streams help readability vs when a simple loop is clearer
What is a Stream?
A Stream<T> is a lazy sequence of elements supporting bulk operations. It is not a data structure — it reads from a source (list, array, IO lines, generator) once.
List<String> names = List.of("ana", "bob", "carl");
List<String> result = names.stream()
.filter(n -> n.length() > 3)
.map(String::toUpperCase)
.toList();
// [BOB, CARL]
Lazy: nothing runs until a terminal operation (toList, count, forEach) executes.
Single-use: after terminal op, stream is consumed — create a new one from the source.
Pipeline anatomy
| Stage | Examples | Notes |
|---|---|---|
| Source | list.stream(), Arrays.stream, Stream.of, IntStream.range |
|
| Intermediate | filter, map, flatMap, sorted, peek, distinct |
return new stream; lazy |
| Terminal | toList, collect, reduce, count, forEach, findFirst |
triggers execution |
long count = users.stream()
.filter(User::active)
.map(User::department)
.distinct()
.count();
Order matters for performance: filter before map when the predicate is cheaper than the mapping.
map and flatMap
map — one output per input:
List<Integer> lengths = words.stream()
.map(String::length)
.toList();
flatMap — one-to-many, then flatten:
List<String> tokens = lines.stream()
.flatMap(line -> Arrays.stream(line.split("\\s+")))
.toList();
Use flatMap when each element expands to a stream (optional values, nested lists, parsing).
Primitive streams
Avoid boxing overhead with IntStream, LongStream, DoubleStream:
int sum = IntStream.rangeClosed(1, 100).sum();
OptionalDouble avg = scores.stream()
.mapToInt(Integer::intValue)
.average();
Convert back with boxed() when you need a Stream<Integer>.
Collecting results
Map<String, List<User>> byDept = users.stream()
.collect(Collectors.groupingBy(User::department));
Map<String, User> byId = users.stream()
.collect(Collectors.toMap(User::id, Function.identity()));
Stream.toList() (Java 16+) — unmodifiable list. Use collect(Collectors.toList()) when you need a mutable ArrayList.
List<String> mutable = stream.collect(Collectors.toCollection(ArrayList::new));
reduce and optional aggregates
Optional<String> longest = words.stream()
.reduce((a, b) -> a.length() >= b.length() ? a : b);
int total = numbers.stream()
.reduce(0, Integer::sum); // with identity
min/max/sum on primitive streams are often clearer than manual reduce.
Parallel streams
long count = hugeList.parallelStream()
.filter(this::expensiveCheck)
.count();
Use sparingly. Parallelism helps CPU-bound work on large in-memory sources with cheap splitting. Wrong cases: small lists, IO-bound tasks, order-dependent side effects, shared mutable state.
The common ForkJoinPool backs parallelStream() — heavy parallel work can starve other parts of the app.
Side effects and peek
forEach is for side effects at the end. peek debugs intermediate stages — do not rely on it for business logic.
Bad:
List<String> acc = new ArrayList<>();
stream.filter(...).peek(acc::add).count(); // fragile, order-dependent
Good:
List<String> acc = stream.filter(...).toList();
Common mistakes
- Missing terminal operation — pipeline does nothing; looks like a bug.
- Reusing a consumed stream —
IllegalStateException. - Modifying source during stream — undefined behavior for most sources.
Collectors.toMapwithout merge function —IllegalStateExceptionon duplicate keys.- Parallel streams with synchronized blocks — defeats parallelism; fix data structure or stay sequential.
- Using stream for tiny collections — loop may be shorter and faster.
Practice checkpoint
-
Flatten
List<List<Integer>>into one list of integers — which op? Answer:flatMap(List::stream)orflatMap(Collection::stream). -
stream.filter(x -> x > 0).count()then reuse same stream — what happens? Answer:IllegalStateException— stream already consumed. -
Group employees by department into lists — which collector? Answer:
Collectors.groupingBy(Employee::department). -
When is parallel stream a bad idea? Answer: small data, IO-bound ops, ordering requirements, or mutating shared state.
Interview angles
- Lazy evaluation: intermediate ops fuse; one pass when possible.
- Stream vs Collection: stream is algorithm; collection is storage.
- toList vs collect: unmodifiable vs customizable mutable result.
- flatMap + Optional:
opt.stream()pattern for chaining nullable steps (often clearer withOptionalmethods directly).