Zum Hauptinhalt springen

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:

  1. What does filter keep?
  2. What does map produce?
  3. What does flatMap flatten?
  4. How does the same idea appear on Optional?
  5. 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:

map wraps; flatMap unwraps 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:

  • limit before sorted is “first 10, then sort those”
  • sorted before limit is “sort all, then take 10”
  • distinct after map to 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

map wraps; flatMap unwraps one level.

filter keeps; type stays the same.

A SQL join is a flatMap of rows. Do it in the database when the data is large.

Next: Week 7 Day 4 — collect, groupingBy, and reduce