Definition
- Signature –
mapMulti(BiConsumer: for each input element, push zero, one, or many output elements to the downstream Consumer.>) - vs flatMap – flatMap allocates a Stream object per element (even for 0 or 1 output). mapMulti calls
consumer.accept()directly. - Best for – conditional emission (0 or 1 element per input), imperative loops, and type casting.
- Primitive variants –
mapMultiToInt,mapMultiToLong,mapMultiToDoubleavoid boxing.
flatMap: each clerk creates a tray of items and puts the tray on the belt. mapMulti: each clerk places items directly on the belt one by one. For one or zero items per clerk, skipping the tray saves work.
Understanding the Concepts
What is mapMulti?
import java.util.List;
import java.util.stream.Stream;
import java.math.BigDecimal;
record Trade(String id, BigDecimal amount, boolean isSplit, List<Trade> legs) {}
// Expand block trades into constituent legs
List<Trade> expanded = trades.stream()
.<Trade>mapMulti((trade, downstream) -> {
if (trade.isSplit()) {
trade.legs().forEach(downstream::accept); // push each leg
} else {
downstream.accept(trade); // push the trade itself
}
})
.toList();
When is mapMulti more efficient than flatMap?
// flatMap: creates a Stream per element (even for single-element output)
List<Trade> result = trades.stream()
.flatMap(t -> t.isSplit()
? t.legs().stream() // Stream created
: Stream.of(t)) // Stream.of creates a Stream for ONE element
.toList();
// mapMulti: no intermediate Stream objects
List<Trade> result = trades.stream()
.<Trade>mapMulti((t, downstream) -> {
if (t.isSplit()) {
t.legs().forEach(downstream::accept);
} else {
downstream.accept(t);
}
})
.toList();
What is the comparison table?
| Aspect | flatMap | mapMulti |
|--------|---------|----------|
| API | Returns Stream per element | Pushes to Consumer |
| Allocation | Stream per element | No Stream allocation |
| Best for | Collection::stream mapping | Conditional 0/1 output |
| Readability | Functional, declarative | Imperative, more verbose |
| Type inference | Natural | Often needs type witness |
How do you use mapMulti for type filtering?
// Filter and cast in one step (no intermediate Stream)
List<String> strings = objects.stream()
.<String>mapMulti((obj, downstream) -> {
if (obj instanceof String s) {
downstream.accept(s); // pattern matching + push
}
})
.toList();
// Equivalent flatMap (more verbose for this pattern)
List<String> strings = objects.stream()
.filter(String.class::isInstance)
.map(String.class::cast)
.toList();
How do you use primitive variants?
// mapMultiToInt: push primitive ints without boxing
int total = trades.stream()
.mapMultiToInt((trade, downstream) -> {
if (trade.isSplit()) {
trade.legs().forEach(leg -> downstream.accept(leg.quantity()));
} else {
downstream.accept(trade.quantity());
}
})
.sum();
// Zero Integer wrapper objects created
What about pushing zero elements (filtering)?
// mapMulti can emit 0 elements (filtering)
List<Trade> settledOnly = trades.stream()
.<Trade>mapMulti((trade, downstream) -> {
if (trade.isSettled()) {
downstream.accept(trade); // 1 element
}
// else: 0 elements pushed (filtered out)
})
.toList();
For simple filtering, filter(Trade::isSettled) is cleaner. mapMulti filtering is useful when combined with transformation.
What are common mistakes?
// WRONG: forgetting the type witness
// trades.stream().mapMulti((t, downstream) -> { ... }); // may not compile
// CORRECT: type witness helps inference
trades.stream().<Trade>mapMulti((t, downstream) -> {
downstream.accept(t);
}).toList();
// WRONG: using mapMulti when flatMap is clearer
trades.stream().<Trade>mapMulti((t, downstream) -> {
t.legs().forEach(downstream::accept);
}).toList();
// CLEANER: flatMap when source naturally provides a Stream
trades.stream().flatMap(t -> t.legs().stream()).toList();
What should a senior engineer know?
- mapMulti is a performance optimization. Prefer flatMap for readability when each element naturally maps to a collection.
- The type witness
is often needed to help type inference.mapMulti - mapMultiToInt avoids boxing for numeric outputs.
- mapMulti is an intermediate operation (lazy) and works with parallel streams.
- Use flatMap for the 80% case. Use mapMulti for the 20% where conditional emission or imperative loops dominate.
Visual Overview
flatMap vs mapMulti:
flatMap: element → Stream → flatten
Trade(split) → Stream(leg1, leg2) → leg1, leg2
Trade(normal) → Stream.of(trade) → trade
Per-element Stream allocation!
mapMulti: element → push to Consumer
Trade(split) → accept(leg1), accept(leg2)
Trade(normal) → accept(trade)
Zero Stream allocation!
Key Takeaway
Stream.mapMulti (Java 16) pushes elements to a Consumer imperatively instead of returning a Stream like flatMap. It is more efficient for conditional emission (0 or 1 element per input) because flatMap allocates a Stream object per element even for single-element output. mapMulti just calls consumer.accept() directly.
Use flatMap when the mapping naturally produces a Stream (Collection::stream). Use mapMulti when the mapping is conditional, involves imperative loops, or mostly produces 0 or 1 element. Primitive variants (mapMultiToInt, mapMultiToLong, mapMultiToDouble) avoid boxing.
Remember one sentence: mapMulti pushes elements imperatively to a Consumer, avoiding the per-element Stream allocation that flatMap creates.
---