skip to content

How would you use flatMap to flatten a nested collection and split text into words, and why can't map do it?

level: middleimportance: should knowfreq 70%

answer

  1. flatMap(List::stream) flattens List<List<X>>
  2. split + Arrays.stream + flatMap = tokenize
  3. map gives Stream<String[]> (nested); flatMap gives Stream<String>
  4. flatMap == map then concat the streams
  5. empty inner stream -> element dropped

basics

~20 s

Use flatMap with a function that turns each element into a stream — e.g. List::stream to flatten a List<List<X>>, or splitting each line into a stream of words. map can't because it would give one nested stream per element (a stream of streams) instead of merging them.

solid answer

~40 s

Flattening means collapsing a one-level-deep nested structure into a flat sequence. With flatMap you give a function that returns a stream for each element, and flatMap concatenates those streams. To flatten List<List<Order>>, call flatMap(List::stream) to get Stream<Order>. To tokenize text, call lines.flatMap(line -> Arrays.stream(line.split(" "))) so each line contributes several words to one combined word stream. map cannot do this: map produces exactly one output per input, so mapping each line to its array/stream of words yields Stream<String[]> or Stream<Stream<String>> — still nested. flatMap is precisely map followed by a concat of the resulting streams. A bonus is that an element can map to an empty stream, so flatMap also drops elements naturally, behaving like a fused map plus filter.

go deeper

for a junior

Can flatten a List<List<X>> with flatMap(List::stream) given the pattern.

for a middle

Independently writes the tokenization pipeline, explains why map leaves a Stream<String[]>, and knows to wrap split() in Arrays.stream.

for a senior

Explains flatMap as map+concat, leverages the zero-output filter trick, and applies the same idea to Optional.flatMap.

for a principal

Weighs flatMap allocation overhead vs mapMulti for hot paths and reasons about ordering/short-circuiting interactions when flattening.

## What 'flatten' means **Flattening** is removing one level of nesting from a data structure: turning a *collection of collections* into a single collection. For example `[[1,2],[3],[4,5]]` flattens to `[1,2,3,4,5]`. In stream terms, going from `Stream<List<X>>` (or `Stream<Stream<X>>`) down to `Stream<X>`. ## Why map alone fails `map` applies a `Function<T,R>` and yields **one** `R` per element. If the natural transformation of an element is itself *many* values, the only way to return 'many' as one `R` is to wrap them in a collection or stream. So you get one of these nested results: ```java Stream<List<Integer>> nested = ...; nested.map(List::stream); // Stream<Stream<Integer>> -- a stream of streams ``` You now have to flatten anyway, so `map` hasn't solved the problem. ## flatMap = map + concatenate `flatMap` takes a `Function<T, Stream<R>>`. For each input element it produces a **whole stream**; `flatMap` then **concatenates** (joins end to end) all of those streams into a single `Stream<R>`. Conceptually: > flatMap(f) == map(f) then 'concat all the resulting streams'. ### Example 1 — flattening nested collections ```java List<List<Order>> perCustomer = ...; List<Order> all = perCustomer.stream() .flatMap(List::stream) // each List<Order> -> Stream<Order>, concatenated .collect(Collectors.toList()); ``` ### Example 2 — splitting text into words ```java List<String> lines = List.of("the quick fox", "jumps high"); List<String> words = lines.stream() .flatMap(line -> Arrays.stream(line.split(" "))) // line -> Stream<String> .collect(Collectors.toList()); // [the, quick, fox, jumps, high] ``` Each line expands into several words (one-to-many), and `flatMap` merges those word-streams into one flat word stream. Doing the same with `map(line -> line.split(" "))` would give `Stream<String[]>` — one array per line, still nested. ## The zero-output bonus Because the per-element function can return an **empty** stream, an element can contribute **zero** outputs. That makes `flatMap` a fused **map + filter**: ```java Stream.of("1","x","3") .flatMap(s -> isNumber(s) ? Stream.of(Integer.parseInt(s)) : Stream.empty()) .forEach(System.out::println); // 1, 3 ("x" dropped) ``` ## Primitive and Optional cousins - Primitive: `flatMapToInt`, etc., when the inner stream is an `IntStream`. - `Optional.flatMap` flattens `Optional<Optional<X>>` to `Optional<X>` the same way — useful when chaining lookups that each return an `Optional`. ## Takeaway Reach for `flatMap` whenever the transformation is one-to-many (or zero-to-many) — flattening nested collections, tokenizing, expanding ranges — and reach for `map` when it's strictly one-to-one.

  • Why wrap split()'s result in Arrays.stream inside flatMap?
    split() returns a String[]; flatMap needs a Stream, so Arrays.stream(arr) adapts the array into a Stream<String> for each line.
  • How can flatMap behave like a filter?
    By returning Stream.empty() for elements you want to drop and Stream.of(value) for those you keep, flatMap emits zero or one element, fusing mapping and filtering.

saying these in an interview costs you the question

  • Using map(line -> line.split(" ")) and expecting flat words
  • Believing you must nest two streams to tokenize
  • Forgetting Arrays.stream around split() so you pass an array, not a stream
  • Thinking flatMap deduplicates or sorts

context