The Streams API

flatMap & Mapping

15 min Lesson 5 of 13

flatMap & Mapping

You already know that map() transforms each element of a stream into something else — one input, one output. But what happens when a single element naturally expands into many elements? That is exactly the problem flatMap() solves. Understanding when and why to reach for flatMap instead of map is one of the most important skills in working with the Streams API.

The Problem: Nested Structures

Imagine you have a list of sentences and you want a stream of every individual word. With a plain map() you get a Stream<String[]> — a stream of arrays — not a flat stream of strings:

List<String> sentences = List.of("hello world", "streams are powerful"); // map gives you Stream<String[]> — NOT what you want Stream<String[]> wrong = sentences.stream() .map(s -> s.split(" "));

Every sentence got turned into an array. You now have a stream of arrays, and iterating it hands you arrays, not individual words. flatMap() fixes this by first mapping each element to a stream, then flattening all those sub-streams into one continuous stream.

The mental model: map is one-to-one. flatMap is one-to-many — each source element produces a stream of results, and those streams are all merged into the single output stream.

flatMap in Action

List<String> sentences = List.of("hello world", "streams are powerful"); List<String> words = sentences.stream() .flatMap(s -> Arrays.stream(s.split(" "))) .collect(Collectors.toList()); System.out.println(words); // [hello, world, streams, are, powerful]

The lambda passed to flatMap must return a Stream. Here Arrays.stream(s.split(" ")) produces a small stream for each sentence. The API merges all those streams into one before any downstream operations see it.

Flattening a List of Lists

A classic use case is a nested collection — for example, each department holds a list of employees:

record Department(String name, List<String> employees) {} List<Department> departments = List.of( new Department("Engineering", List.of("Alice", "Bob", "Carol")), new Department("Marketing", List.of("Dave", "Eve")), new Department("Finance", List.of("Frank")) ); List<String> allEmployees = departments.stream() .flatMap(d -> d.employees().stream()) .sorted() .collect(Collectors.toList()); System.out.println(allEmployees); // [Alice, Bob, Carol, Dave, Eve, Frank]

With map you would have gotten a Stream<List<String>>. With flatMap you get a single Stream<String> — every employee in every department, ready for further operations like sorted() or distinct().

Combining flatMap with Other Operations

Because flatMap produces a regular stream, you can chain any downstream operation after it. A common pattern is to flatten, filter, and then collect:

List<List<Integer>> matrix = List.of( List.of(1, 2, 3), List.of(4, 5, 6), List.of(7, 8, 9) ); // all even numbers from the entire matrix, doubled List<Integer> result = matrix.stream() .flatMap(Collection::stream) .filter(n -> n % 2 == 0) .map(n -> n * 2) .collect(Collectors.toList()); System.out.println(result); // [4, 8, 12, 16]
Use Collection::stream as a method reference instead of writing list -> list.stream() when the element type is a known collection. It is more concise and equally readable.

flatMap vs map: Choosing Correctly

  • If the mapper returns a single transformed value, use map.
  • If the mapper returns a collection or stream of values that should be merged into the main stream, use flatMap.
  • If you use map when you needed flatMap, your element type becomes Stream<Stream<T>> or Stream<List<T>> — a common compile-time clue you have the wrong one.

Numeric Variants: flatMapToInt, flatMapToLong, flatMapToDouble

Just like map has mapToInt to avoid boxing, flatMap has primitive-specialised variants. If your sub-stream produces int values, use flatMapToInt to stay unboxed:

List<int[]> arrays = List.of( new int[]{1, 2, 3}, new int[]{4, 5} ); int sum = arrays.stream() .flatMapToInt(Arrays::stream) // produces IntStream .sum(); System.out.println(sum); // 15
Do not flatten lazily into a Stream<Integer> when you have primitive arrays. Each int gets boxed to Integer, which adds unnecessary heap allocation. Prefer flatMapToInt and stay on IntStream for numeric work.

Real-World Example: Extracting Tags

Consider a blogging platform where each post has a list of tags. You want the top 5 most frequently used tags across all posts:

record Post(String title, List<String> tags) {} List<Post> posts = List.of( new Post("Streams 101", List.of("java", "streams", "tutorial")), new Post("Lambda Guide", List.of("java", "lambdas", "tutorial")), new Post("OOP Patterns", List.of("java", "oop", "design")) ); posts.stream() .flatMap(p -> p.tags().stream()) .collect(Collectors.groupingBy(tag -> tag, Collectors.counting())) .entrySet().stream() .sorted(Map.Entry.<String, Long>comparingByValue().reversed()) .limit(5) .forEach(e -> System.out.println(e.getKey() + ": " + e.getValue())); // java: 3 // tutorial: 2 // ...

Summary

flatMap is the tool for flattening one-to-many relationships in a stream pipeline. The key rule: your mapper must return a Stream, and all those sub-streams are automatically merged into one. Whenever you find yourself mapping to a collection and then struggling to work with a stream of collections, reach for flatMap. For primitive work, flatMapToInt, flatMapToLong, and flatMapToDouble eliminate boxing overhead.