Working with mapMulti() – Functional style programming – extending API

178. Working with mapMulti()

Starting with JDK 16, the Stream API was enriched with a new intermediate operation, named mapMulti(). This operation is represented by the following default method in the Stream interface:

default <R> Stream<R> mapMulti​(
  BiConsumer<? super T,? super Consumer<R>> mapper)

Let’s follow the learning-by-example approach and let’s consider the next classical example that uses a combination of filter() and map() to filter even integers and double their value:

List<Integer> integers = List.of(3, 2, 5, 6, 7, 8);
List<Integer> evenDoubledClassic = integers.stream()
  .filter(i -> i % 2 == 0)
  .map(i -> i * 2)
  .collect(toList());

The same result can be obtained via mapMulti() as follows:

List<Integer> evenDoubledMM = integers.stream()
  .<Integer>mapMulti((i, consumer) -> {
     if (i % 2 == 0) {
       consumer.accept(i * 2);
     }
  })
  .collect(toList());

So, instead of using two intermediate operations, we used only one, mapMulti(). The filter() role was replaced by an if statement, and the map() role is accomplished in the accept() method. This time, we filtered the evens and doubled their values via mapper which is a BiConsumer<? super T,​? super Consumer<R>>. This bi-function is applied to each integer (each stream element), and only the even integers are passed to the consumer. This consumer acts as a buffer that simply passes downstream (in the stream pipeline) the received elements. The mapper.accept(R r) can be called any number of times, which means that, for a given stream element, we can produce as many output elements as we need. In the previous example, we have a one-to-zero mapping (when the i % 2 == 0 is evaluated as false), and a one-to-one mapping (when the i % 2 == 0 is evaluated as true).

More precisely, mapMulti() gets an input stream of elements and outputs another stream containing 0, less, the same, or a larger number of elements that can be unaltered or replaced by other elements. This means that each element from the input stream can pass through a one-to-zero, one-to-one, or one-to-many mapping.

Have you noticed the <Integer>mapMulti(…) type-witness applied to the returned value? Without this type-witness the code will not compile because the compiler cannot determine the proper type of R. This is the shortcoming of using mapMulti(), so, we have to pay this price.For primitive types (double, long, and int) we have mapMultiToDouble(), mapMultiToLong(), and mapMultiToInt() which return DoubleStream, LongStream, and IntStream. For instance, if we plan to sum the even integers then using mapMultiToInt() is a better choice than mapMulti() since we can skip the type-witness and work only with primitive int:

int evenDoubledAndSumMM = integers.stream()
  .mapMultiToInt((i, consumer) -> {
     if (i % 2 == 0) {
       consumer.accept(i * 2);
     }
  })
  .sum();

On the other hand, whenever you need a Stream<T> instead of Double/Long/IntStream, you still need to rely on mapToObj() or boxed():

List<Integer> evenDoubledMM = integers.stream()
  .mapMultiToInt((i, consumer) -> {
    if (i % 2 == 0) {
      consumer.accept(i * 2);
    }
  })
  .mapToObj(i -> i) // or, .boxed()
  .collect(toList());

Once you get familiar with mapMulti() you start to realize that it is pretty similar to the well-known flatMap() which is useful to flatten a nested Stream<Stream<R>> model. Let’s consider the following one-to-many relationship:

public class Author {
  private final String name;
  private final List<Book> books;
  …
}
public class Book {
   
  private final String title;
  private final LocalDate published;
  …
}