Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 20

Flattening Nested Collections with flatMap

Recommended resources

Hello! Welcome to the fourth lesson in our "Functional Java with Streams & Lambdas" module.

In our last lesson, we mastered the fundamental trio of stream operations: filter, map, and reduce. You learned how map performs a one-to-one transformation, taking each element of a stream and converting it into a single new element.

Today, we address a common and crucial interview scenario that map alone can't handle elegantly: what happens when you need to perform a one-to-many transformation? For example, how do you get a single list of all phone numbers from a list of customers, where each customer has their own list of phone numbers?

This lesson's goal is to equip you to solve problems involving nested collections using the flatMap operation. Mastering flatMap is essential for flattening complex data structures into simple, processable streams, a skill frequently tested in mid-to-senior Java interviews.

1. The Challenge of Nested Collections

Let's start with a practical problem. Imagine you have a list of employees, and each employee has worked in multiple cities. Your task is to compile a single, consolidated list of all unique cities where your employees have experience.

Using what we learned in the last lesson, you might try using map:

class Employee {
    // ... constructor, getters
    private String name;
    private List<String> citiesWorkedIn;
}

List<Employee> employees = // ... list of employees

// Let's try to get all cities using map()
List<List<String>> citiesByEmployee = employees.stream()
                                               .map(Employee::getCitiesWorkedIn)
                                               .collect(Collectors.toList());

System.out.println(citiesByEmployee); 
// Output might be: [[Pune, Mumbai], [Pune, Bangalore], [Noida, Gurgaon]]

As you can see, map does its job perfectly: it transforms each Employee object into a List<String>. The result is a List<List<String>>, a list of lists. This is a nested collection and not the flat List<String> of unique cities we wanted.

This is precisely the problem flatMap is designed to solve.

Map Vs Flat Map | Top Java 8 Coding Interview Question and Answers | Code Decode | Java 8 features

To see a live demonstration of this exact problem, watch this segment from the 'Map Vs Flat Map' video by Code Decode. It clearly illustrates why map falls short when dealing with nested lists.

Watch from 08:27 to 10:44. The video shows an attempt to get all cities from a list of employees, resulting in a List<List<String>>, which perfectly frames the need for flatMap.


2. Understanding flatMap: The Flattening Transformer

flatMap is best understood as a two-step operation:

  1. Map: It applies a function to each element, just like map. However, this function must return a Stream of new elements.
  2. Flat: It then takes all of these generated streams and "flattens" them into a single, combined stream.

Let's look at their signatures to understand the key difference:

  • map(Function<T, R>): The provided function transforms an element of type T into a single element of type R.
  • flatMap(Function<T, Stream<R>>): The provided function transforms an element of type T into a stream of elements of type R.

In Java 8, what's the difference between Stream.map() and ...

The top-voted answer on this Stack Overflow page provides one of the clearest explanations of the difference between map and flatMap. Pay close attention to the explanation of the function signatures and the concept of 'flattening'.

Read the first two answers on the page. The first, by Stuart Marks, explains the core difference in terms of what the mapping function returns (one value vs. a stream of values). The second answer provides a great analogy for 'flattening' a two-level structure like [ [1,2,3],[4,5,6] ] into a one-level structure [ 1,2,3,4,5,6 ].

The core idea is that flatMap allows one element in the input stream to be mapped to zero, one, or many elements in the output stream, which are then all seamlessly combined.


3. flatMap in Action: Solving the Nested Collection Problem

Now, let's solve our initial problem of getting a flat list of cities using flatMap.

List<String> allCities = employees.stream()
    .flatMap(employee -> employee.getCitiesWorkedIn().stream()) // 1
    .distinct() // 2
    .collect(Collectors.toList()); // 3

System.out.println(allCities);
// Output will be: [Pune, Mumbai, Bangalore, Noida, Gurgaon]

Let's break down the pipeline:

  1. .flatMap(employee -> employee.getCitiesWorkedIn().stream()): For each Employee, we get their List<String> of cities and then call .stream() on it. This turns the List<String> into a Stream<String>. flatMap then merges all these small streams into one large Stream<String>.
  2. .distinct(): This is an intermediate operation that removes duplicate elements from the stream.
  3. .collect(Collectors.toList()): The terminal operation gathers the unique city names into our final List<String>.

A Common Interview Scenario: Customer and PhoneNumbers

Another classic example you'll find in interviews involves a Customer class with a list of phone numbers.

Java 8 Streams | map () & flatMap() Example | JavaTechie

The 'Java 8 Streams | map () & flatMap() Example' video by Java Techie provides an excellent, step-by-step walkthrough of this exact scenario. It's a must-watch to solidify your understanding.

Watch from 04:11 to 12:51. The video first shows why map results in a List<List<String>> for phone numbers and then refactors it to use flatMap to get the correct, flattened List<String>.

The pattern is always the same: when you have a Stream<A> and A contains a Collection<B>, you use flatMap to get a Stream<B>.

Test your understanding!

You are given a List<String>, where each string is a sentence (e.g., "the quick brown fox"). Write a stream pipeline to create a single List<String> containing all the individual words from all the sentences.

Show answer
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class WordExtractor {
    public static void main(String[] args) {
        List<String> sentences = List.of("the quick brown fox", "jumps over the lazy dog");

        List<String> words = sentences.stream()
                .flatMap(sentence -> Arrays.stream(sentence.split(" ")))
                .collect(Collectors.toList());

        System.out.println(words);
        // Output: [the, quick, brown, fox, jumps, over, the, lazy, dog]
    }
}

Explanation: sentence.split(" ") returns a String[]. Arrays.stream() converts this array into a Stream<String>, which flatMap can then use to merge all the word streams into one.


4. Interview Problem Workshop: Combining Operations

In interviews, you'll often be asked to solve problems that require combining flatMap with other operations like map or filter.

Problem: Given a list of lists of integers, return a single list containing the square of each number.

Input: List.of(List.of(1, 2), List.of(3, 4, 5), List.of(6))
Expected Output: [1, 4, 9, 16, 25, 36]

How would you build the pipeline?

  1. First, you need to flatten the List<List<Integer>> into a single stream of integers. This is a job for flatMap.
  2. Then, you need to transform each integer in the flattened stream into its square. This is a one-to-one transformation, so it's a job for map.

Top 10 Java map() and flatMap() Functional Programming ...

The article 'Top 10 Java map() and flatMap() Functional Programming ...' from Java Revisited provides several excellent interview problems. Let's look at how they solve this exact one.

Scroll down to 'Problem 8' in the article. Analyze the solution provided. Note the order of operations: flatMap is used first to create a unified stream, and then map is applied to each element of that new stream.

The solution demonstrates a powerful and common pattern:

List<List<Integer>> listOfLists = List.of(List.of(1, 2), List.of(3, 4, 5), List.of(6));

List<Integer> squaredNumbers = listOfLists.stream()
    .flatMap(List::stream) // Flattens List<List<Integer>> to Stream<Integer>
    .map(n -> n * n)       // Squares each number
    .collect(Collectors.toList());

System.out.println(squaredNumbers);

By using flatMap and map together, you can perform complex transformations on nested data structures with clean, declarative code.


Conclusion

You've now added another critical tool to your Java Streams toolkit. While map is your go-to for one-to-one transformations, flatMap is essential for handling one-to-many scenarios and flattening nested data structures.

Key Takeaways for Your Interview:

  • Recognize the Pattern: If an interview problem involves a structure like List<List<T>> or a List<User> where each user has a List<Purchase>, and the goal is to process all T's or Purchase's in a single collection, flatMap is almost certainly part of the solution.
  • Map vs. FlatMap: Be ready to articulate the difference clearly. map's function returns a single value (T -> R). flatMap's function must return a Stream (T -> Stream<R>). flatMap effectively maps and then flattens.
  • Build Complex Pipelines: Show your proficiency by combining flatMap with map, filter, and distinct to solve multi-step problems. The order matters: flatten first, then transform or filter the individual elements.

Preview of the Next Lesson:
We've learned how to transform and flatten streams. The next logical step is to organize the elements of a stream into more complex data structures. In our next lesson, we will explore how to group and partition collection data effectively using built-in collectors like groupingBy and partitioningBy, allowing you to create Maps from your streams.