1. Introduction
Let’s imagine we have a list of students, and each student has a list of hobbies. For example:
List<List<String>> hobbies = List.of(
List.of("Swimming", "Chess"),
List.of("Soccer"),
List.of("Programming", "Reading", "Movies")
);
Your task: get a single list of all hobbies to find out what students are interested in. It’s logical to try using map:
List<Stream<String>> streams = hobbies.stream()
.map(list -> list.stream())
.collect(Collectors.toList());
What did we get? A list of streams! That is, Stream<Stream<String>>. That’s not quite what we need — we’d like to get just a Stream<String> to work with each hobby directly.
Imagine you have a box containing other boxes with toys. The map method just takes out each box and shows you the box (Stream<Stream<String>>). But you want to see all the toys at once (Stream<String>) without digging into boxes.
2. flatMap: how to flatten nested collections
To “unpack the boxes” and get a flat stream, you need the flatMap method.
It takes a function that returns a stream for each element and immediately “flattens” all nested streams into one.
flatMap syntax
Stream<T> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
Simply put: flatMap expects you to return a stream (Stream) for each element, and it will “smooth” all of that into one large stream.
Example: merging all student hobbies
List<String> allHobbies = hobbies.stream()
.flatMap(list -> list.stream())
.collect(Collectors.toList());
System.out.println(allHobbies);
// [Swimming, Chess, Soccer, Programming, Reading, Movies]
For each list of hobbies, we called list.stream() (we got a stream of hobbies for one student). And then flatMap “smoothed” all streams into one big stream — now we see all hobbies, not streams of hobbies.
Visual scheme
hobbies.stream()
|
|---> [Swimming, Chess] -> stream
|---> [Soccer] -> stream
|---> [Programming, ...] -> stream
|
flatMap: combines everything into a single Stream<String>
Why doesn’t map solve the task?
If we used map, we’d get Stream<Stream<String>>, which is inconvenient: for example, you cannot immediately iterate over all strings — you’d need extra passes.
3. Practical examples of using flatMap
Splitting strings into characters (List<String> → Stream<Character>)
Suppose you have a list of strings, and you want to get a stream of all characters that appear across all strings:
List<String> words = List.of("Java", "Stream");
List<Character> characters = words.stream()
.flatMap(word -> word.chars().mapToObj(ch -> (char) ch))
.collect(Collectors.toList());
System.out.println(characters);
// [J, a, v, a, S, t, r, e, a, m]
Explanation:
- word.chars() returns an IntStream (a stream of code points).
- mapToObj turns code points into characters.
- flatMap merges all resulting character streams into one.
Working with Optional: Stream<Optional<T>> → Stream<T>
Suppose you have a list of Optional objects, and you want to get a stream of only those values that are actually present:
List<Optional<String>> optionals = List.of(
Optional.of("Java"),
Optional.empty(),
Optional.of("Stream")
);
List<String> present = optionals.stream()
.flatMap(opt -> opt.stream())
.collect(Collectors.toList());
System.out.println(present);
// [Java, Stream]
Tip:
Optional<T> (since Java 9) has a stream() method that returns either an empty stream or a stream with a single element. flatMap combines all non-empty values into one stream.
4. The new mapMulti method: when flatMap isn’t needed
Why was mapMulti introduced?
Introduced in Java 16, mapMulti works similarly to flatMap but is a bit more efficient and flexible.
flatMap is a powerful tool, but it has a drawback: for each element you are required to create a new Stream, even if you want to return 0, 1, or several elements. This can be inefficient, especially if you just want to “expand” elements without creating intermediate collections or streams.
mapMulti is an improved version of flatMap that lets you add values directly to the resulting stream via a Consumer, without creating intermediate structures. Instead of returning a stream, you specify which elements to add to the resulting stream via the Consumer.
mapMulti syntax
<R> Stream<R> mapMulti(BiConsumer<? super T, ? super Consumer<R>> mapper)
For each element, the mapper function is called, receiving the element itself and a Consumer into which you can “put” new values.
Example: filtering and expanding elements in a single pass
Suppose you have a list of numbers, and you want to add even numbers to the stream twice, and odd numbers not at all (i.e., filtering and “expanding” at the same time):
List<Integer> numbers = List.of(1, 2, 3, 4);
List<Integer> result = numbers.stream()
.mapMulti((number, consumer) -> {
if (number % 2 == 0) {
consumer.accept(number);
consumer.accept(number); // Add twice
}
// Do nothing for odd numbers (filtering)
})
.collect(Collectors.toList());
System.out.println(result);
// [2, 2, 4, 4]
Comparison with flatMap
The same task with flatMap would look like this:
List<Integer> result = numbers.stream()
.flatMap(number -> number % 2 == 0
? Stream.of(number, number)
: Stream.empty())
.collect(Collectors.toList());
Here we still have to create Stream.of(...) or Stream.empty() for each element, even if it’s inefficient.
5. When to use flatMap vs mapMulti?
- map — when you transform each element into a single element.
- flatMap — when you transform each element into a stream of elements.
- mapMulti — when you want to generate multiple elements without creating an intermediate stream (more efficient, especially in hot loops).
Another example: flattening Map<Integer, List<String>> into Stream<Pair(Integer, String)>
Suppose we have a map: id → list of hobbies.
Map<Integer, List<String>> studentHobbies = Map.of(
1, List.of("Swimming", "Chess"),
2, List.of("Soccer"),
3, List.of("Programming", "Reading", "Movies")
);
List<String> allHobbies = studentHobbies.values().stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
System.out.println(allHobbies);
// [Swimming, Chess, Soccer, Programming, Reading, Movies]
And if we want to get pairs (id, hobby):
List<String> pairs = studentHobbies.entrySet().stream()
.flatMap(entry -> entry.getValue().stream()
.map(hobby -> entry.getKey() + ": " + hobby))
.collect(Collectors.toList());
System.out.println(pairs);
// [1: Swimming, 1: Chess, 2: Soccer, 3: Programming, 3: Reading, 3: Movies]
Using mapMulti (Java 16+):
List<String> pairs = studentHobbies.entrySet().stream()
.mapMulti((entry, consumer) -> {
for (String hobby : entry.getValue()) {
consumer.accept(entry.getKey() + ": " + hobby);
}
})
.collect(Collectors.toList());
Advantage:
mapMulti doesn’t create intermediate streams; it simply pushes values directly into the resulting stream.
Visualization: flatMap and mapMulti
| Method | What the function returns | How it combines | Intermediate collections? |
|---|---|---|---|
|
Single element | Simple | No |
|
A stream (Stream) | Concatenates | Yes (intermediate Streams) |
|
Consumer (0..n times) | Concatenates | No (adds directly) |
6. Common mistakes when working with flatMap and mapMulti
Error #1: Got Stream<Stream<T>> instead of Stream<T>. Students often use map instead of flatMap when working with collections of collections. As a result, they have to write extra loops.
Error #2: Incorrect return type. For flatMap, the function must return a Stream, not a List or an array.
Error #3: Inefficiency. In simple cases, flatMap works great, but if you have to create Stream.of or Stream.empty() for each element, that can be excessive. For such tasks, it’s better to use mapMulti.
Error #4: mapMulti doesn’t work in older Java versions. mapMulti appeared only in Java 16. If your JDK version is older, this method won’t be available.
Error #5: Issues with null. Do not return null from flatMap — always return Stream.empty() for “empty” cases.
Error #6: Intermediate collections. Don’t create extra lists or streams if you can add elements directly via a consumer in mapMulti.
GO TO FULL VERSION