1. Overview
In this tutorial, we’ll review the method Stream::mapMulti introduced in Java 16. We’ll write simple examples to illustrate how to use it. In particular, **we’ll see that this method is similar to Stream::flatMap. We’ll cover under what circumstances we prefer to use mapMulti over flatMap.
Be sure to check out our articles on Java Streams for a deeper dive into the Stream API.
2. Method Signature
Omitting the wildcards, the mapMulti method can be written more succinctly:
<R> Stream<R> mapMulti(BiConsumer<T, Consumer<R>> mapper)
It’s a Stream intermediate operation. It requires as a parameter the implementation of a BiConsumer functional interface. *The implementation of the BiConsumer takes a Stream element T, if necessary, transforms it into type R, and invokes the mapper’s Consumer::accept.*
Inside Java’s mapMulti method implementation, the mapper is a buffer that implements the Consumer functional interface.
Each time we invoke Consumer::accept, it accumulates the elements in the buffer and passes them to the stream pipeline.
3. Simple Implementation Example
Let’s consider a list of integers to do the following operation:
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
double percentage = .01;
List<Double> evenDoubles = integers.stream()
.<Double>mapMulti((integer, consumer) -> {
if (integer % 2 == 0) {
consumer.accept((double) integer * ( 1 + percentage));
}
})
.collect(toList());
In our lambda implementation of BiConsumer<T, Consumer
As we saw before, the consumer is just a buffer that passes the return elements to the stream pipeline. (As a side note, notice that we have to use a type witness
This is either a one-to-zero or one-to-one transformation depending on whether the element is odd or even.
Notice that the if statement in the previous code sample plays the role of a Stream::filter, and casting the integer into a double, the role of a Stream::map. Hence, we could use Stream’s filter and map to achieve the same result:
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
double percentage = .01;
List<Double> evenDoubles = integers.stream()
.filter(integer -> integer % 2 == 0)
.<Double>map(integer -> ((double) integer * ( 1 + percentage)))
.collect(toList());
However, the mapMulti implementation is more direct since we don’t need to invoke so many stream intermediate operations.
Another advantage is that the mapMulti implementation is imperative, giving us more freedom to do element transformations.
To support int, long, and double primitive types, we have mapMultiToDouble, mapMultiToInt, and mapMultiToLong variations of mapMulti.
For example, we can use mapMultiToDouble to find the sum of the previous List of doubles:
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
double percentage = .01;
double sum = integers.stream()
.mapMultiToDouble((integer, consumer) -> {
if (integer % 2 == 0) {
consumer.accept(integer * (1 + percentage));
}
})
.sum();
4. More Realistic Example
Let’s consider a collection of Albums:
public class Album {
private String albumName;
private int albumCost;
private List<Artist> artists;
Album(String albumName, int albumCost, List<Artist> artists) {
this.albumName = albumName;
this.albumCost = albumCost;
this.artists = artists;
}
// ...
}
Each Album has a list of Artists:
public class Artist {
private final String name;
private boolean associatedMajorLabels;
private List<String> majorLabels;
Artist(String name, boolean associatedMajorLabels, List<String> majorLabels) {
this.name = name;
this.associatedMajorLabels = associatedMajorLabels;
this.majorLabels = majorLabels;
}
// ...
}
If we want to collect a list of artist-album name pairs, we can implement it using mapMulti:
List<Pair<String, String>> artistAlbum = albums.stream()
.<Pair<String, String>> mapMulti((album, consumer) -> {
for (Artist artist : album.getArtists()) {
consumer.accept(new ImmutablePair<String, String>(artist.getName(), album.getAlbumName()));
}
})
For each album in the stream, we iterate over the artists, create an Apache Commons ImmutablePair of artist-album names, and invoke Consumer::accept. The implementation of mapMulti accumulates the elements accepted by the consumer and passes them to the stream pipeline.
This has the effect of a one-to-many transformation where the results are accumulated in the consumer but ultimately are flattened into a new stream. This is essentially what Stream::flatMap does so that we can achieve the same result with the following implementation:
List<Pair<String, String>> artistAlbum = albums.stream()
.flatMap(album -> album.getArtists()
.stream()
.map(artist -> new ImmutablePair<String, String>(artist.getName(), album.getAlbumName())))
.collect(toList());
We see that both methods give identical results. We’ll cover next in which cases it is more advantageous to use mapMulti.
5. When to Use mapMulti Instead of flatMap
5.1. Replacing Stream Elements with a Small Number of Elements
As stated in the Java documentation: “when replacing each stream element with a small (possibly zero) number of elements. Using this method avoids the overhead of creating a new Stream instance for every group of result elements, as required by flatMap”.
Let’s write a simple example that illustrates this scenario:
int upperCost = 9;
List<Pair<String, String>> artistAlbum = albums.stream()
.<Pair<String, String>> mapMulti((album, consumer) -> {
if (album.getAlbumCost() < upperCost) {
for (Artist artist : album.getArtists()) {
consumer.accept(new ImmutablePair<String, String>(artist.getName(), album.getAlbumName()));
}
}
})
For each album, we iterate over the artists and accumulate zero or few artist-album pairs, depending on the album’s price compared with the variable upperCost.
To accomplish the same results using flatMap:
int upperCost = 9;
List<Pair<String, String>> artistAlbum = albums.stream()
.flatMap(album -> album.getArtists()
.stream()
.filter(artist -> upperCost > album.getAlbumCost())
.map(artist -> new ImmutablePair<String, String>(artist.getName(), album.getAlbumName())))
.collect(toList());
We see that the imperative implementation of mapMulti is more performant — we don’t have to create intermediate streams with each processed element as we do with the declarative approach of flatMap.
5.2. When It’s Easier to Generate Result Elements
Let’s write in the Album class a method that passes all the artist-album pairs with their associated major labels to a consumer:
public class Album {
//...
public void artistAlbumPairsToMajorLabels(Consumer<Pair<String, String>> consumer) {
for (Artist artist : artists) {
if (artist.isAssociatedMajorLabels()) {
String concatLabels = artist.getMajorLabels().stream().collect(Collectors.joining(","));
consumer.accept(new ImmutablePair<>(artist.getName()+ ":" + albumName, concatLabels));
}
}
}
// ...
}
If the artist has an association with major labels, the implementation joins the labels into a comma-separated string. It then creates a pair of artist-album names with the labels and invokes Consumer::accept.
If we want to get a list of all the pairs, it’s as simple as using mapMulti with the method reference Album::artistAlbumPairsToMajorLabels:
List<Pair<String, String>> copyrightedArtistAlbum = albums.stream()
.<Pair<String, String>> mapMulti(Album::artistAlbumPairsToMajorLabels)
.collect(toList());
We see that, in more complex cases, we could have very sophisticated implementations of the method reference. For instance, the Java documentation gives an example using recursion.
In general, replicating the same results using flatMap will be very difficult. Therefore, we should use mapMulti in cases where generating result elements is much easier than returning them in the form of a Stream as required in flatMap.
6. Conclusion
In this tutorial, we’ve covered how to implement mapMulti with different examples. We’ve seen how it compares with flatMap and when it’s more advantageous to use.
In particular, it’s recommended to use mapMulti when a few stream elements need to be replaced or when it’s easier to use an imperative approach to generate the elements of the stream pipeline.
The source code can be found over on GitHub.