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:
- Map: It applies a function to each element, just like
map. However, this function must return aStreamof new elements. - 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 typeTinto a single element of typeR.flatMap(Function<T, Stream<R>>): The provided function transforms an element of typeTinto a stream of elements of typeR.
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:
.flatMap(employee -> employee.getCitiesWorkedIn().stream()): For eachEmployee, we get theirList<String>of cities and then call.stream()on it. This turns theList<String>into aStream<String>.flatMapthen merges all these small streams into one largeStream<String>..distinct(): This is an intermediate operation that removes duplicate elements from the stream..collect(Collectors.toList()): The terminal operation gathers the unique city names into our finalList<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?
- First, you need to flatten the
List<List<Integer>>into a single stream of integers. This is a job forflatMap. - 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 aList<User>where each user has aList<Purchase>, and the goal is to process allT's orPurchase's in a single collection,flatMapis 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 aStream(T -> Stream<R>).flatMapeffectively maps and then flattens. - Build Complex Pipelines: Show your proficiency by combining
flatMapwithmap,filter, anddistinctto 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.