The flatMap() operation is an intermediate operation in the Java Stream API used when one stream element can produce multiple values and those values need to be combined into a single stream.
If map() is a transformation station that converts one item into another item, flatMap() goes one step further: it can transform one item into multiple items and then flatten the result into a single stream.
Why Do We Need flatMap()?
Consider a list containing several lists of programming languages. If you use map(), each inner list remains a separate element. The result is therefore still nested.
import java.util.List;
public class MapNestedList {
public static void main(String[] args) {
List<List<String>> groups = List.of(
List.of("Java", "Spring"),
List.of("SQL", "Docker"),
List.of("Git", "Maven")
);
List<List<String>> result = groups.stream()
.map(group -> group)
.toList();
System.out.println(result);
}
}
The result is still a List<List<String>>. But suppose the application needs one stream containing all six technologies. This is where flatMap() becomes useful.
import java.util.List;
public class FlatMapExample {
public static void main(String[] args) {
List<List<String>> groups = List.of(
List.of("Java", "Spring"),
List.of("SQL", "Docker"),
List.of("Git", "Maven")
);
List<String> result = groups.stream()
.flatMap(List::stream)
.toList();
System.out.println(result);
}
}
Now the nested lists are flattened into one stream of strings. This is the central purpose of flatMap().
Important: flatMap() is useful when a single input element can correspond to zero, one, or multiple output elements and those outputs should be processed as one continuous stream.
Syntax of flatMap()
The general syntax is:
stream.flatMap(function)
The function receives one element and returns a stream. Java then combines the resulting streams into one flattened stream.
Stream<R> flatMap(
Function<T, ? extends Stream<? extends R>> mapper
)
| Part | Meaning |
|---|---|
| T | Input element type |
| R | Output element type |
| Function | Converts each input element into a stream |
| flatMap() | Combines those streams into one stream |
Understanding the Difference Between map() and flatMap()
The easiest way to remember the difference is to look at the shape of the result.
| Operation | Input | Output Shape |
|---|---|---|
| map() | One element | One transformed element |
| flatMap() | One element | Multiple elements that are flattened |
For example, if each employee has a list of skills, map() can produce a stream of skill lists, while flatMap() can produce one stream containing every individual skill.
Basic flatMap() Example
Let's start with a simple nested collection.
import java.util.List;
public class BasicFlatMap {
public static void main(String[] args) {
List<List<Integer>> numbers = List.of(
List.of(1, 2, 3),
List.of(4, 5, 6),
List.of(7, 8, 9)
);
numbers.stream()
.flatMap(List::stream)
.forEach(System.out::println);
}
}
Each inner list is converted into a stream. flatMap() then joins all those streams into one stream of integers.
Conceptually, the transformation looks like this:
[ [1, 2, 3], [4, 5, 6], [7, 8, 9] ]
↓ flatMap()
[ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Using map() Instead
To understand why flatMap() exists, compare it with map().
List<List<Integer>> numbers = List.of(
List.of(1, 2, 3),
List.of(4, 5, 6)
);
numbers.stream()
.map(List::stream)
.forEach(System.out::println);
Here, map() produces a stream of streams. The nested structure remains.
numbers.stream()
.flatMap(List::stream)
.forEach(System.out::println);
With flatMap(), the nested streams are flattened into one stream of integers.
Remember: map() preserves nesting when the mapping function produces a collection or stream. flatMap() removes that extra level of nesting.
flatMap() with Strings
Suppose each sentence contains several words and you want one stream containing every word.
import java.util.List;
import java.util.Arrays;
public class SentenceWords {
public static void main(String[] args) {
List<String> sentences = List.of(
"Java is powerful",
"Streams are useful",
"Learning takes practice"
);
sentences.stream()
.flatMap(sentence ->
Arrays.stream(sentence.split(" "))
)
.forEach(System.out::println);
}
}
Each sentence is converted into a stream of words. flatMap() then combines all those word streams into one stream.
flatMap() with Object Collections
A very common real-world situation is an object containing a collection of related objects.
import java.util.List;
public class EmployeeSkills {
record Employee(
String name,
List<String> skills
) {}
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee(
"Amit",
List.of("Java", "Spring")
),
new Employee(
"Priya",
List.of("SQL", "Docker")
),
new Employee(
"Rahul",
List.of("JavaScript", "React")
)
);
List<String> skills = employees.stream()
.flatMap(employee -> employee.skills().stream())
.toList();
System.out.println(skills);
}
}
Each employee contains a list of skills. The stream starts with employees, but flatMap() turns the individual skill lists into one continuous stream of skills.
Filtering After flatMap()
Once nested data has been flattened, normal stream operations can be applied directly to the individual elements.
List<String> javaSkills = employees.stream()
.flatMap(employee -> employee.skills().stream())
.filter(skill -> skill.equalsIgnoreCase("Java"))
.toList();
System.out.println(javaSkills);
The flattening step exposes individual skills to the rest of the pipeline, allowing filter() to work on each skill directly.
Mapping Before flatMap()
You can combine map() and flatMap() when the nested data needs to be transformed before it is flattened.
import java.util.List;
public class MapThenFlatMap {
record Team(String name, List<String> members) {}
public static void main(String[] args) {
List<Team> teams = List.of(
new Team("Backend", List.of("Amit", "Rahul")),
new Team("Frontend", List.of("Priya", "Neha"))
);
teams.stream()
.map(Team::members)
.flatMap(List::stream)
.forEach(System.out::println);
}
}
The map() operation extracts the member lists, and flatMap() then combines those lists into one stream of member names.
Filtering Before flatMap()
You can also filter parent objects before flattening their nested data. This can be useful when only certain parent records are relevant.
teams.stream()
.filter(team -> team.name().equals("Backend"))
.flatMap(team -> team.members().stream())
.forEach(System.out::println);
Only the Backend team is selected. Its members are then flattened into the stream.
flatMap() with Empty Collections
One useful property of flatMap() is that an empty stream contributes no elements to the final stream.
import java.util.List;
public class EmptyNestedCollection {
record Employee(String name, List<String> skills) {}
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Amit", List.of("Java", "Spring")),
new Employee("Priya", List.of()),
new Employee("Rahul", List.of("Docker"))
);
employees.stream()
.flatMap(employee -> employee.skills().stream())
.forEach(System.out::println);
}
}
Priya's empty skill list contributes nothing to the final stream. The other employees' skills continue normally.
Handling Potential Null Collections
A common production problem occurs when a nested collection may be null. Calling stream() on a null collection causes a NullPointerException.
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
employees.stream()
.flatMap(employee ->
employee.skills() == null
? Stream.empty()
: employee.skills().stream()
)
.forEach(System.out::println);
An alternative is to design the domain model so that collection properties are never null and use an empty collection when there are no values. That approach is usually easier to maintain.
Production Tip: Prefer empty collections over null collections when designing APIs and domain objects. It reduces defensive code throughout the application.
flatMap() with Optional
The idea of flattening also appears when working with Optional. If a mapping operation produces another Optional, flatMap() can avoid creating nested Optional structures.
import java.util.Optional;
public class OptionalFlatMap {
public static void main(String[] args) {
Optional<String> name = Optional.of("Amit");
Optional<String> result = name.flatMap(
value -> Optional.of(value.toUpperCase())
);
System.out.println(result);
}
}
The stream and Optional APIs are different, but the underlying idea is similar: flatten a nested result rather than creating another level of wrapping.
flatMap() with Sets
The nested collection does not have to be a list. Any collection that can produce a stream can participate in a flattening pipeline.
import java.util.List;
import java.util.Set;
public class SetFlatMap {
public static void main(String[] args) {
List<Set<String>> groups = List.of(
Set.of("Java", "Spring"),
Set.of("Docker", "Kubernetes")
);
groups.stream()
.flatMap(Set::stream)
.forEach(System.out::println);
}
}
The resulting stream contains the individual set elements rather than separate sets.
flatMap() with Arrays
Arrays can also be flattened by converting each array into a stream.
import java.util.Arrays;
import java.util.List;
public class ArrayFlatMap {
public static void main(String[] args) {
List<String[]> arrays = List.of(
new String[]{"Java", "Spring"},
new String[]{"SQL", "Docker"}
);
arrays.stream()
.flatMap(array -> Arrays.stream(array))
.forEach(System.out::println);
}
}
Each array becomes a stream, and flatMap() combines those streams into one sequence.
flatMap() and Duplicate Values
flatMap() does not automatically remove duplicates. If the nested collections contain repeated values, those values remain in the resulting stream.
List<List<String>> groups = List.of(
List.of("Java", "Spring"),
List.of("Java", "Docker")
);
groups.stream()
.flatMap(List::stream)
.forEach(System.out::println);
If unique values are required, add distinct() after flattening:
groups.stream()
.flatMap(List::stream)
.distinct()
.forEach(System.out::println);
flatMap() and Stream Depth
It is important to understand that flatMap() flattens one level of nested stream structure created by that mapping operation. It does not magically flatten arbitrarily deep nested data.
List<List<String>> data = List.of(
List.of("Java", "Spring"),
List.of("SQL", "Docker")
);
List<String> result = data.stream()
.flatMap(List::stream)
.toList();
For ordinary application data, one level of flattening is usually exactly what you need. For deeply nested structures, the data model or processing approach should be reconsidered rather than adding confusing layers of stream operations.
Common Beginner Mistakes
- Using map() when flattening is required: If the mapping function returns a stream or collection and you need individual elements, consider flatMap().
- Expecting flatMap() to remove duplicates: Flattening and deduplication are separate operations. Use distinct() when uniqueness is required.
- Ignoring null nested collections: Calling stream() on a null collection causes an exception.
- Flattening data unnecessarily: If the nested structure is meaningful to the business logic, flattening it may destroy useful structure.
- Using complicated nested lambdas: Move complicated transformations into named methods when they become difficult to read.
- Confusing flatMap() with map(): Remember that map transforms one input into one result, while flatMap transforms an input into a stream whose elements are merged into the surrounding stream.
Best Practices
- Use flatMap() when nested streams need to become one stream.
- Keep flattening logic simple and readable.
- Prefer empty collections instead of null collections in domain models.
- Filter parent objects before flattening when irrelevant parent records can be eliminated early.
- Use distinct() separately when duplicate removal is part of the requirement.
- Do not flatten data merely because you can; preserve nesting when that structure carries business meaning.
- Use method references such as List::stream when they make the pipeline clearer.
Interview Insights
Question: What is flatMap() in Java Streams?
Answer: flatMap() transforms each stream element into a stream and then combines the resulting streams into one flattened stream.
Question: What is the difference between map() and flatMap()?
Answer: map() transforms each element into one result, while flatMap() allows each element to produce a stream of values and flattens those values into one stream.
Question: Is flatMap() an intermediate operation?
Answer: Yes. flatMap() is an intermediate stream operation and is lazy until a terminal operation consumes the pipeline.
Question: Does flatMap() remove duplicates?
Answer: No. It only flattens nested streams. Use distinct() when duplicate elements must be removed.
Question: When should flatMap() be used?
Answer: Use it when each input element contains or can produce multiple values and those values need to be processed as one continuous stream.
Quick Revision
| Concept | Key Point |
|---|---|
| flatMap() | Transforms elements into streams and flattens the results |
| Type | Intermediate operation |
| Input | One stream element |
| Mapper result | A Stream of output elements |
| Primary use | Flatten nested collections or streams |
| Duplicates | Not automatically removed |
| Null collections | Should be handled explicitly or avoided by design |
| map() comparison | map preserves nested structure; flatMap flattens it |
| Common pattern | Parent objects → nested collections → individual elements |
Final Takeaway
The flatMap() operation becomes essential whenever your data contains a collection inside another collection or an object contains multiple related values that need to be processed together. The key idea is simple: map transforms; flatMap transforms and flattens. Once you understand that distinction, tasks such as collecting all employee skills, extracting words from sentences, combining product categories, or processing nested API data become much easier to express with clean Java stream pipelines.
