Replacing Each Element of a Stream with Multiple Elements – Streams

Replacing Each Element of a Stream with Multiple Elements

The mapMulti() intermediate operation applies a one-to-many transformation to the elements of the stream and flattens the result elements into a new stream. The functionality of the mapMulti() method is very similar to that of the flatMap() method. Whereas the latter uses a Function<T, Stream<R>> mapper to create a mapping stream for each element and then flattens the stream, the former applies a BiConsumer<T, Consumer<R>> mapper to each element. The mapper calls the Consumer to accept the replacement elements that are incorporated into a single stream when the pipeline is executed.

The mapMulti() method can be used to perform filtering, mapping, and flat mapping of stream elements, all depending on the implementation of the BiConsumer mapper passed to the method.

The code below shows a one-to-one transformation of the stream elements. A BiConsumer is defined at (1) that first filters the stream for pop music CDs at (2), and maps each CD to a string that contains its title and its number of tracks represented by an equivalent number of “*” characters. The resulting string is submitted at (3) to the consumer (supplied by the mapMulti() method). Each value passed to the accept() method of the consumer replaces the current element in the stream. Note that the body of the BiConsumer is implemented in an imperative manner using an if statement. The BiConsumer created at (1) is passed to the mapMulti() method at (5) to process the CDs of the stream created at (4). The mapMulti() method passes an appropriate Consumer to the BiConsumer that accepts the replacement elements.

Click here to view code image

// One-to-one
BiConsumer<CD, Consumer<String>> bcA = (cd, consumer) -> {              // (1)
  if (cd.genre() == Genre.POP) {                                        // (2)
    consumer.accept(String.format(“%-15s: %s”, cd.title(),              // (3)
                                  “*”.repeat(cd.noOfTracks())));
  }
};
CD.cdList.stream()                                                      // (4)
          .mapMulti(bcA)                                                // (5)
          .forEach(System.out::println);

Output from the code:

Java Jive      : ********
Lambda Dancing : **********

The code below shows a one-to-many transformation of the stream elements. The BiConsumer at (1) iterates through a list of CDs and maps each CD in the list to its title. Each list of CDs in the stream will thus be replaced with the titles of the CDs in the list. The mapMulti() operation with the BiConsumer at (1) is applied at (3) to a stream of list of CDs (Stream<List<CD>>) created at (2). The mapMulti() operation in this case is analogous to the flatMap() operation to achieve the same result.

Click here to view code image

// One-to-many
List<CD> cdList1 = List.of(CD.cd0, CD.cd1, CD.cd1);
List<CD> cdList2 = List.of(CD.cd0, CD.cd1);
BiConsumer<List<CD>, Consumer<String>> bcB = (lst, consumer) -> {       // (1)
  for (CD cd : lst) {
    consumer.accept(cd.title());
  }
};
List<String> listOfCDTitles = Stream.of(cdList1, cdList2) // (2) Stream<List<CD>>
    .mapMulti(bcB)                                        // (3)
    .distinct()
    .toList();
System.out.println(listOfCDTitles);                       // [Java Jive, Java Jam]

The previous two code snippets first defined the BiConsumer with all relevant types specified explicitly, and then passed it to the mapMulti() method. The code below defines the implementation of the BiConsumer in the call to the mapMulti() method. We consider three alternative implementations as exemplified by (2a), (2b), and (2c).

Alternative (2a) results in a compile-time error. The reason is that the compiler cannot unequivocally infer the actual type parameter R of the consumer parameter of the lambda expression. It can only infer that the type of the lst parameter is List<CD> as it denotes an element of stream whose type is Stream<List<CD>>. The compiler makes the safest assumption that the type parameter R is Object. With this assumption, the resulting list is of type List<Object>, but this cannot be assigned to a reference of type List<String>, as declared in the assignment statement. To avoid the compile-time error in this case, we can change the type of the reference to Object or to the wildcard ?.

Alternative (2b) uses the type witness <String> in the call to the mapMulti() method to explicitly corroborate the actual type parameter of the consumer.

Alternative (2c) explicitly specifies the types for the parameters of the lambda expression.

Click here to view code image

List<String> listOfCDTitles2 = Stream.of(cdList1,cdList2) // (1) Stream<List<CD>>
//  .mapMulti((lst, consumer) -> {                    // (2a) Compile-time error!
//  .<String>mapMulti((lst, consumer) -> {                     // (2b) OK.
    .mapMulti((List<CD> lst, Consumer<String> consumer) -> {   // (2c) OK.
      for (CD cd : lst) {
        consumer.accept(cd.title());
      }
    })
    .distinct()
    .toList();
System.out.println(listOfCDTitles2);                  // [Java Jive, Java Jam]

The mapMulti() method is preferable to the flatMap() method under the following circumstances:

  • When an element is to be replaced with a small number of elements, or none at all. The mapMulti() method avoids the overhead of creating a mapped stream for each element, as done by the flatMap() method.
  • When an imperative approach for creating replacement elements is easier than using a stream.

The following default method is defined in the Stream<T> interface, and an analogous method is also defined in the IntStream, LongStream, and DoubleStream interfaces:

Click here to view code image

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

Returns a stream that is a result of replacing each element of this stream with multiple elements, specifically zero or more elements.

The specified mapper is applied to each element in conjunction with a consumer that accepts replacement elements. The mapper calls the consumer zero or more times to accept the replacement elements.

Note that the consumer is supplied by the mapMulti() method, and called by the mapper to accept replacement elements. An element of type T is replaced with zero or more elements of type R.

This is an intermediate operation that changes the stream size and the element type of the stream, and does not guarantee to preserve the encounter order of the input stream.

The following default methods are defined only in the Stream<T> interface. No counterparts exist in the IntStream, LongStream, or DoubleStream interfaces:

Click here to view code image

default IntStream
       mapMultiToInt(BiConsumer<? super T,? super IntConsumer> mapper)
default LongStream
       mapMultiToLong(BiConsumer<? super T,? super LongConsumer> mapper)
default DoubleStream
       mapMultiToDouble(BiConsumer<? super T,? super DoubleConsumer> mapper)

Return an IntStream, LongStream, and DoubleStream, respectively, consisting of the results of replacing each element of this stream with multiple elements, specifically zero or more elements.

Leave a Reply

Your email address will not be published. Required fields are marked *