Back to Stream: flatMap
Q4Question 4 of 7

Stream.mapMulti (Java 16): push 0..N elements imperatively, vs flatMap

Key Concepts
  • mapMulti pushes elements to a downstream Consumer imperatively instead of creating a Stream per element
  • More efficient than flatMap when producing 0 or 1 element per input (no Stream.of/empty overhead)
  • Useful for imperative logic: loops, conditions, multiple conditional outputs
  • Primitive variants: mapMultiToInt, mapMultiToLong, mapMultiToDouble avoid boxing
  • Java 16+ via JEP (added as API enhancement, no separate JEP number)
  • Use flatMap when the mapping naturally produces a Stream (Collection::stream)
  • Use mapMulti when the mapping is conditional or involves imperative loops

Definition

Stream.mapMulti pushes 0 to N elements to a Consumer imperatively instead of creating a Stream per element like flatMap, avoiding per-element stream allocation overhead.
  • 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, mapMultiToDouble avoid 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?

JAVA
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?

JAVA
// 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?

JAVA
// 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?

JAVA
// 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)?

JAVA
// 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?

JAVA
// 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 mapMulti is often needed to help type inference.
  • 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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate