Java Stream mapMulti() with Examples and flatMap Comparison

Learn Java Stream mapMulti() with examples, the type witness it needs, when it beats flatMap() and when it does not, primitive variants and a real case.

Comparison of map, flatMap and mapMulti showing how each input element becomes output elements

The Stream.mapMulti() method, added in Java 16, replaces each stream element with zero, one or more elements by passing them to a Consumer instead of returning a new stream. It does the same one-to-many job as flatMap(), but we write the logic as a plain loop or if block that calls accept() for every element we want to emit.

We use mapMulti() to filter and transform in one step, to flatten nested collections with a condition, to unwrap Optional values and to turn one event into several events. The following example emits each number together with its square and its cube.

List<Integer> numbers = List.of(1, 2, 3);

List<Integer> powers = numbers.stream()
        .<Integer>mapMulti((num, downstream) -> {
            downstream.accept(num);
            downstream.accept(num * num);
            downstream.accept(num * num * num);
        })
        .toList();
List<Integer> result = powers;                  // [1, 1, 1, 2, 4, 8, 3, 9, 27]

Notice the <Integer> before mapMulti, which tells the compiler the element type of the new stream. We look at the syntax, explain the type witness, show when mapMulti() is better than flatMap() and when it is not, cover the primitive variants and finish with a transaction alert example.

1. Syntax

The mapMulti() method is an intermediate operation of the Java Stream API. It takes a BiConsumer, a functional interface that receives the current element and a Consumer for the output stream. Each call to accept() on that consumer puts one element into the resulting stream.

// does not compile: signature as declared in the Stream<T> interface
<R> Stream<R> mapMulti(BiConsumer<? super T, ? super Consumer<R>> mapper)

Compared with map(), which produces one output element for every input element, mapMulti() can produce any number, including none. It is closest to flatMap(), which also flattens many outputs into one stream, but flatMap() needs a Stream object per input element.

Comparison of map, flatMap and mapMulti showing how each input element becomes output elements
map() gives one output per input, flatMap() returns a stream per input, and mapMulti() pushes zero or more outputs into a consumer

In application code, the lambda has two parameters, the input element and the consumer, and it returns nothing.

Stream<String> words = Stream.of("tea", "", "jam");
List<String> letters = words
        .<String>mapMulti((word, downstream) -> {
            for (char c : word.toCharArray()) {
                downstream.accept(String.valueOf(c));
            }
        })
        .toList();
List<String> chars = letters;                   // [t, e, a, j, a, m]

The empty string adds nothing to the result, because the loop body never runs for it. The consumer is valid only while the lambda runs, so storing it in a field and calling it later gives undefined results.

2. A Simple Example

A good first use of mapMulti() is a filter and a map that belong together. In the following example, we keep only the even numbers and emit each of them twice, once as is and once multiplied by ten. With filter() and flatMap(), the same result needs two operations and a Stream.of() call per element.

List<Integer> numbers = List.of(1, 2, 3, 4);

List<Integer> evensAndTens = numbers.stream()
        .<Integer>mapMulti((n, downstream) -> {
            if (n % 2 == 0) {
                downstream.accept(n);
                downstream.accept(n * 10);
            }
        })
        .toList();
List<Integer> withMapMulti = evensAndTens;       // [2, 20, 4, 40]
List<Integer> withFlatMap = numbers.stream().filter(n -> n % 2 == 0).flatMap(n -> Stream.of(n, n * 10)).toList();   // [2, 20, 4, 40]

We can choose to emit any number of output elements per input, or none at all, inside the same lambda.

3. Providing the Result Type With a Type Witness

The compiler cannot infer the output type R from a lambda that only calls accept(). Without help, it picks Object, so the pipeline produces a Stream<Object> and assigning the result to a List<Integer> fails.

// does not compile: R is inferred as Object
List<Integer> doubled = Stream.of(1, 2)
        .mapMulti((n, downstream) -> downstream.accept(n * 2))
        .toList();

There are two fixes. We either put the type witness <Integer> in front of the method name or declare the lambda parameter types, where the consumer type carries the output type.

List<Integer> witness = Stream.of(1, 2).<Integer>mapMulti((n, downstream) -> downstream.accept(n * 2)).toList();   // [2, 4]
List<Integer> typed = Stream.of(1, 2).mapMulti((Integer n, Consumer<Integer> downstream) -> downstream.accept(n * 2)).toList();   // [2, 4]

The type witness is shorter and is the form most code uses. A method reference such as Optional::ifPresent does not fix the inference either, so section 4.3 also uses <String>.

4. When to Use mapMulti() Method

There are two situations where mapMulti() is preferable to flatMap(). The first is when each element turns into a small number of elements, possibly zero, because mapMulti() avoids creating a new Stream for each group. The second is when it is easier to generate the results with imperative code, such as loops and if statements, than to return them as a stream.

4.1. Data Transformation

We transform each input element into one or more output elements based on a condition, and skip the elements that do not qualify. A common case is “keep only the elements of a certain type and cast them”, for example a stream of Number objects that should give only the Integer values. Combined with pattern matching for instanceof, the check and the cast fit into one line.

List<Number> readings = List.of(1, 2.5, 3, 4L, 5);

List<Integer> ints = readings.stream()
        .<Integer>mapMulti((n, downstream) -> {
            if (n instanceof Integer i) {
                downstream.accept(i);
            }
        })
        .toList();
List<Integer> onlyInts = ints;                   // [1, 3, 5]

Parsing user input works the same way. We try to parse each string and emit the number only when parsing succeeds, so bad input is skipped instead of stopping the stream with a NumberFormatException.

List<String> input = List.of("10", " 25 ", "abc", "", "7");

List<Integer> parsed = input.stream()
        .<Integer>mapMulti((s, downstream) -> {
            try {
                downstream.accept(Integer.parseInt(s.strip()));
            } catch (NumberFormatException e) {
                // skip invalid values; log them in real code
            }
        })
        .toList();
List<Integer> valid = parsed;                    // [10, 25, 7]

4.2. Flattening Nested Collections

We have a stream of objects that hold collections and want one flat stream of the inner elements that meet a condition. With flatMap(), we need an inner stream and a filter(). With mapMulti(), a loop with an if does both, and we can also check a condition on the outer object before the loop.

record Order(String status, List<String> items) {}
List<Order> orders = List.of(
        new Order("PAID", List.of("tea", "mug")),
        new Order("CANCELLED", List.of("jam")),
        new Order("PAID", List.of("cake", "")));

List<String> toPack = orders.stream()
        .<String>mapMulti((order, downstream) -> {
            if (order.status().equals("PAID")) {
                for (String item : order.items()) {
                    if (!item.isBlank()) {
                        downstream.accept(item);
                    }
                }
            }
        })
        .toList();
List<String> packList = toPack;                  // [tea, mug, cake]

The flatMap() version, orders.stream().filter(o -> o.status().equals(“PAID”)).flatMap(o -> o.items().stream()).filter(i -> !i.isBlank()), is equally correct and shorter to read for most developers. More patterns for nested data, including keeping the parent next to each match, are in filtering nested collections with streams.

4.3. Unwrapping Optional Values

A stream of Optional objects, for example the results of several cache lookups, often needs to become a stream of the present values. The method reference Optional::ifPresent matches the BiConsumer shape of mapMulti(), because it takes the optional and a consumer and calls the consumer only when a value is present.

List<Optional<String>> lookups = List.of(Optional.of("tea"), Optional.empty(), Optional.of("jam"));

List<String> found = lookups.stream().<String>mapMulti(Optional::ifPresent).toList();   // [tea, jam]
List<String> viaFlatMap = lookups.stream().flatMap(Optional::stream).toList();           // [tea, jam]

Both lines give the same result. The flatMap(Optional::stream) form, available since Java 9, is the more common idiom, while mapMulti(Optional::ifPresent) skips creating a one-element stream for every present value.

5. mapMulti() vs flatMap()

The flatMap() method stays the default for one-to-many mapping, and mapMulti() is the better choice when each element yields few results or when a loop expresses the logic more clearly than a stream. The table compares the two on the points that matter in code reviews.

AspectflatMap()mapMulti()
SinceJava 8Java 16
Mapper returnsA Stream per elementNothing; calls accept() zero or more times
StyleDeclarative, chains of stream operationsImperative, loops and if blocks
Output type inferenceInferred from the returned streamOften needs a type witness such as <Integer>
Per-element overheadCreates a stream object, even an empty oneNo intermediate stream
Short-circuiting with findFirst() or limit()Stops pulling from the inner streamThe mapper always runs to the end for the current element
Existing stream per element, such as list.stream()Natural fitNeeds a loop over the collection

The overhead row is the reason to prefer mapMulti() with small result groups. In the JDK, the stream implementation passes the downstream stage to our lambda as the consumer, so no buffer or stream is created. Whether that makes a measurable difference depends on the pipeline and the data, so we measure with a JMH benchmark before changing working flatMap() code for speed.

The short-circuiting row matters for large or expensive expansions. When a pipeline ends with findFirst(), a flatMap() pipeline stops after the first inner element, while a mapMulti() mapper that loops over 1,000 values emits all 1,000 before the stream can stop.

AtomicInteger emitted = new AtomicInteger();
Optional<Integer> first = Stream.of(1, 2)
        .<Integer>mapMulti((n, downstream) -> {
            for (int i = 0; i < 1000; i++) {
                emitted.incrementAndGet();
                downstream.accept(i);
            }
        })
        .findFirst();
int calls = emitted.get();                       // 1000

6. Primitive Variants mapMultiToInt(), mapMultiToLong() and mapMultiToDouble()

To produce a primitive stream without boxing, Stream offers mapMultiToInt(), mapMultiToLong() and mapMultiToDouble(), all added in Java 16. Their consumer is an IntConsumer, LongConsumer or DoubleConsumer, so the output type is fixed and no type witness is needed.

record Basket(String owner, List<Integer> prices) {}
List<Basket> baskets = List.of(new Basket("ana", List.of(3, 5)), new Basket("raj", List.of(10)));

int total = baskets.stream()
        .mapMultiToInt((basket, downstream) -> {
            for (int price : basket.prices()) {
                downstream.accept(price);
            }
        })
        .sum();
int sum = total;                                 // 18
int[] squares = IntStream.of(2, 3).mapMulti((n, downstream) -> downstream.accept(n * n)).toArray();   // [4, 9]

The primitive streams themselves, such as IntStream, have their own mapMulti() that keeps the element type, as the last line shows. More on these streams is in primitive type streams.

7. Stream mapMulti() Example

Let us take an example close to real-world applications. A banking app receives a list of transactions, and for each transaction it must create an event for the activity feed. A withdrawal above 100 also needs a second event, a fraud alert, so one transaction can produce one or two events.

With map(), each transaction can only become one event, and with flatMap() we would build a small list or stream per transaction. The mapMulti() method fits the rule as written, “always emit the activity event, and also emit an alert when the condition holds”.

enum Type { DEPOSIT, WITHDRAWAL }
record Transaction(String id, double amount, Type type) {}
record TransactionEvent(String type, double amount) {}
List<Transaction> transactions = List.of(
        new Transaction("T1", 100, Type.DEPOSIT),
        new Transaction("T2", 50, Type.WITHDRAWAL),
        new Transaction("T3", 200, Type.WITHDRAWAL));

List<TransactionEvent> events = transactions.stream()
        .<TransactionEvent>mapMulti((tx, downstream) -> {
            switch (tx.type()) {
                case DEPOSIT -> downstream.accept(new TransactionEvent("Deposit", tx.amount()));
                case WITHDRAWAL -> {
                    downstream.accept(new TransactionEvent("Withdrawal", tx.amount()));
                    if (tx.amount() > 100) {
                        downstream.accept(new TransactionEvent("Fraud Alert", tx.amount()));
                    }
                }
            }
        })
        .toList();
int eventCount = events.size();                  // 4

We can verify the generated events by printing them.

TransactionEvent[type=Deposit, amount=100.0]
TransactionEvent[type=Withdrawal, amount=50.0]
TransactionEvent[type=Withdrawal, amount=200.0]
TransactionEvent[type=Fraud Alert, amount=200.0]

The type witness <TransactionEvent> is required here. Without it, the result is a Stream<Object> and the assignment to List<TransactionEvent> does not compile. The exhaustive switch over the Type enum also makes the compiler report an error in this lambda when a new transaction type is added.

8. mapMulti() FAQs

Most mapMulti() questions are about the Java version, speed, the inferred type and the newer gatherers.

8.1. Which Java version added mapMulti()?

Java 16. The methods Stream.mapMulti(), mapMultiToInt(), mapMultiToLong() and mapMultiToDouble(), plus mapMulti() on IntStream, LongStream and DoubleStream, all arrived in JDK 16 and are unchanged up to Java 25.

8.2. Is mapMulti() faster than flatMap()?

Sometimes. It skips creating a stream per element, which helps when each element yields zero or a few results. When a pipeline short-circuits, flatMap() can do less work, as section 5 shows, so measure before switching for speed.

8.3. Why does mapMulti() return Stream<Object>?

Because the compiler cannot infer the output type from a lambda that only calls accept(). Add a type witness, .<String>mapMulti(…), or declare the lambda parameter types as in section 3.

8.4. Should we use mapMulti() or a Stream Gatherer?

Use mapMulti() when each element maps to outputs on its own, with no memory of earlier elements. Gatherers, final in Java 24, are for stateful steps such as sliding windows or running totals that depend on the elements seen before.

9. Conclusion

The Stream.mapMulti() method replaces each element with zero or more elements that we push into a consumer. It is the better choice when a filter and a transformation belong together, when each element yields few results, and when a loop or an if block expresses the rule more clearly than a stream.

Remember the type witness, keep flatMap() for cases where a stream per element already exists, and prefer flatMap() in short-circuiting pipelines over large expansions. The difference between map() and flatMap() is covered in map() vs flatMap().

10. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. Good examples.

    The output of the 1st example should be: ‘[1, 1, 1, 2, 4, 8, 3, 9, 27]’
    Please update the comment:
    ‘System.out.println(multiples); // Output: [2, 3, 4, 6, 6, 9, 8, 12, 10, 15]’
    to
    ‘System.out.println(multiples); // Output: [1, 1, 1, 2, 4, 8, 3, 9, 27]’

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.