Java Stream: flatMap vs mapMulti
Last modified: June 8, 2025
When working with Java Streams, developers often need to transform and expand
stream elements. Java provides two primary methods for this:
flatMap
(since Java 8) and mapMulti
(introduced in
Java 16). This tutorial explores their differences, performance characteristics,
and ideal use cases.
While both methods can transform each stream element into zero or more output
elements, they follow different paradigms. flatMap
uses a
functional approach requiring new Stream instances, while mapMulti
uses an imperative consumer-based approach that can be more efficient in certain
scenarios.
Key Differences Overview
The following table summarizes the key differences between
mapMulti
and flatMap
in Java Streams.
Feature | mapMulti | flatMap |
---|---|---|
Introduced | Java 16 | Java 8 |
Programming Style | Imperative (push) | Functional (pull) |
Performance | Better for simple expansions | Better for complex transformations |
Intermediate Objects | No intermediate streams | Creates intermediate streams |
Readability | Better for imperative logic | Better for functional pipelines |
Both methods can achieve similar results, but their performance and readability can vary significantly based on the specific use case. Understanding these differences helps you choose the right tool for your stream processing needs.
Basic Element Expansion
This example shows how both methods can expand each input element into multiple output elements. We'll convert strings to their uppercase and lowercase variants.
void main() { List<String> words = List.of("apple", "banana", "cherry"); List<String> result = words.stream() .flatMap(word -> Stream.of(word.toUpperCase(), word.toLowerCase())) .toList(); System.out.println(result); }
This code uses flatMap
to transform each word into two
elements: its uppercase and lowercase versions. The flatMap
method takes a function that returns a Stream for each input element, which
is then flattened into a single output stream.
The mapMulti
method uses a BiConsumer
to emit multiple
elements for each input. The consumer accepts the transformed elements directly,
avoiding the need to create intermediate Stream
instances.
void main() { List<String> words = List.of("apple", "banana", "cherry"); List<String> result = words.stream() .<String>mapMulti((word, consumer) -> { consumer.accept(word.toUpperCase()); consumer.accept(word.toLowerCase()); }) .toList(); System.out.println(result); }
Both versions produce identical output, but mapMulti
avoids
creating intermediate Stream instances. For simple expansions like this,
mapMulti
often shows better performance while maintaining
readability.
Conditional Element Expansion
When you need to conditionally expand elements based on certain criteria, the differences between the two approaches become more apparent.
void main() { List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6); List<Integer> result = numbers.stream() .flatMap(n -> { if (n % 2 == 0) { return Stream.of(n, n * 10); } return Stream.empty(); }) .toList(); System.out.println(result); }
This code uses flatMap
to transform even numbers into two
elements: the number itself and its tenfold. If the number is odd, it returns
an empty Stream. This approach works, but it can lead to less readable code
when dealing with multiple conditions or complex logic.
void main() { List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6); List<Integer> result = numbers.stream() .<Integer>mapMulti((n, consumer) -> { if (n % 2 == 0) { consumer.accept(n); consumer.accept(n * 10); } }) .toList(); System.out.println(result); }
The mapMulti
version handles conditional logic more naturally
without requiring explicit empty stream returns. This makes the code more
straightforward when dealing with multiple conditions or complex branching
logic.
When to Use Each Approach
The maptMulti
and flatMap
methods each have their
strengths and ideal use cases. Understanding when to use each can help you
write more efficient and readable code.
The mapMulti
method has several advantages over
flatMap
in certain scenarios:
- No intermediate Stream object allocations
- Reduced lambda captures
- Better inlining opportunities for the JIT compiler
The mapMulti
method is particularly useful when you need to
perform simple expansions (1-to-few elements) or when you want to handle
complex conditional logic in a more imperative style. It allows you to
emit elements directly without creating intermediate Stream objects, which can
improve performance in certain scenarios.
Prefer mapMulti when:
- You need imperative control over element emission
- Performing simple expansions (1-to-few elements)
- Handling complex conditional logic
- Working with stateful transformations
- Performance is critical for simple cases
Prefer flatMap when:
- Your transformation naturally returns a Stream
- Working with existing methods that return Streams
- Chaining functional operations
- Readability is more important than minor performance gains
- Transforming to many elements (1-to-many)
Real-world Example: Order Processing
Let's examine a more practical example processing e-commerce orders with nested items.
record Order(String id, List<Item> items) {} record Item(String sku, int quantity, double price) {} void main() { List<Order> orders = List.of( new Order("O1", List.of( new Item("I1", 2, 9.99), new Item("I2", 1, 19.99) )), new Order("O2", List.of( new Item("I3", 5, 4.99) )) ); List<String> result = orders.stream() .flatMap(order -> order.items().stream() .filter(item -> item.quantity() > 1) .map(item -> "%s: %s x %.2f".formatted( order.id(), item.sku(), item.price() * item.quantity() )) ) .toList(); System.out.println(result); }
This code uses flatMap
to transform each word into two
elements: its uppercase and lowercase versions. The flatMap
method takes a function that returns a Stream for each input element, which
is then flattened into a single output stream.
record Order(String id, List<Item> items) {} record Item(String sku, int quantity, double price) {} void main() { List<Order> orders = List.of( new Order("O1", List.of( new Item("I1", 2, 9.99), new Item("I2", 1, 19.99) )), new Order("O2", List.of( new Item("I3", 5, 4.99) )) ); List<String> result = orders.stream() .<String>mapMulti((order, consumer) -> { for (Item item : order.items()) { if (item.quantity() > 1) { String line = "%s: %s x %.2f".formatted( order.id(), item.sku(), item.price() * item.quantity() ); consumer.accept(line); } } }) .toList(); System.out.println(result); }
In this real-world scenario, both approaches work well. The flatMap
version may be preferable when already working with methods that return Streams,
while mapMulti
offers better performance and more straightforward
imperative logic when dealing with complex conditions.
Migration Tips
When migrating from flatMap
to mapMulti
:
- Replace the Stream-returning function with a BiConsumer
- Change element emission from
return Stream.of(x)
toconsumer.accept(x)
- Move filtering logic before the emission rather than using
filter
- For empty results, simply don't call the consumer
Remember that migration isn't always necessary - flatMap
remains a
good choice for many scenarios, especially when working with existing
Stream-returning methods.
Source
Java Stream mapMulti Documentation
Java Stream flatMap Documentation
Both mapMulti
and flatMap
are valuable tools in the
Stream API. mapMulti
provides an imperative alternative that can be
more efficient for certain patterns, while flatMap
remains the more
functional approach. Choose based on your specific requirements, coding style,
and performance needs.
Author
List all Java tutorials.