Week 7 Day 3 — map, flatMap, and filter
Goal
Today I want the three operations I use on almost every pipeline, with a sharp line between wrap and flatten.
Main questions:
- What does
filterkeep? - What does
mapproduce? - What does
flatMapflatten? - How does the same idea appear on
Optional? - How does this relate to one-to-many in JPA?
1. filter
filter(Predicate<T>) keeps elements for which the predicate is true. Type does not change: Stream<Order> stays Stream<Order>.
orders.stream()
.filter(o -> o.status() == Status.OPEN)
.filter(o -> o.total().cents() > 0);
I stack filters or I and predicates. Empty result is an empty stream, not null.
2. map — one to one
map(Function<T,R>) turns each element into one value. Stream<T> becomes Stream<R>.
Stream<Long> ids = orders.stream().map(Order::id);
Stream<OrderResponse> dtos = orders.stream().map(OrderResponse::from);
If the function returns a list, I get Stream<List<Line>> — a stream of lists, not a stream of lines.
Memory sentence:
mapwraps;flatMapunwraps one level.
3. flatMap — one to many, then flatten
flatMap(Function<T, Stream<R>>) turns each element into a stream and concatenates them.
Stream<Line> lines = orders.stream()
.flatMap(o -> o.lines().stream());
order1 { lineA, lineB }
order2 { lineC }
│ flatMap
▼
lineA, lineB, lineC
map of a list plus flatMap(List::stream) is the same idea in two steps. I write flatMap when I already think “each order has many lines.”
Optional uses the same word:
optionalUser.flatMap(User::email); // Optional<Email>
optionalUser.map(User::email); // Optional<Optional<Email>> if email() returns Optional
map on Optional wraps the function’s return. If that return is already Optional, I nest. flatMap unwraps one level.
4. distinct, limit, sorted in the same pipeline
orders.stream()
.flatMap(o -> o.lines().stream())
.map(Line::sku)
.distinct()
.sorted()
.limit(10)
.toList();
Order of operations changes results and cost:
limitbeforesortedis “first 10, then sort those”sortedbeforelimitis “sort all, then take 10”distinctaftermapto sku dedupes skus, not lines
I read the pipeline top to bottom as the story of each element.
5. Spring / JPA connection
A SQL join of orders to lines duplicates parent rows. That is the database’s flatMap. In Java:
orders.stream().flatMap(o -> o.lines().stream())
touches the lines collection. If that collection is lazy and the session is closed, it blows up. Fetch a graph I need in the query (join fetch / entity graph), then stream in memory on a small page.
Do not findAll + flatMap lines as a report on production data. Push flattening to SQL (JOIN) and paginate.
6. Common traps
Trap 1: map(order -> order.lines()) then wondering why I have Stream<List<Line>>.
Trap 2: flatMap with a function that returns null instead of Stream.empty(). NPE. Return Stream.empty().
Trap 3: Nested Optional from map instead of flatMap.
Trap 4: filter that throws on null elements. Filter nulls first or forbid them in the list (Week 6).
Trap 5: Using flatMap to hide a side-effecting save per line. That is a loop with extra steps.
Practice Questions and Answers
Question 1
map vs flatMap?
Answer:
map turns each element into one value; the stream depth stays one. flatMap turns each element into a stream (or an Optional, on Optional) and concatenates, so nested collections become one stream of children. If I map to a List, I get Stream<List<T>>.
Question 2
How do I get every line of every order?
Answer:
orders.stream().flatMap(o -> o.lines().stream()). I need the session open if lines is lazy, or I fetch lines in the query first.
Question 3
Why flatMap on Optional?
Answer:
If User.email() returns Optional<Email>, map(User::email) is Optional<Optional<Email>>. flatMap collapses to Optional<Email>. Same unwrap-one-level rule.
Question 4
Does the order of sorted and limit matter?
Answer:
Yes. sorted().limit(10) sorts everything, then takes ten. limit(10).sorted() takes ten (encounter order), then sorts those ten. Cost and result both change.
Question 5
What should flatMap return when an order has no lines?
Answer:
Stream.empty(), never null. flatMap will call stream() on the result; null NPEs.
Memory sentences
mapwraps;flatMapunwraps one level.
filterkeeps; type stays the same.
A SQL join is a flatMap of rows. Do it in the database when the data is large.