Перейти к основному содержимому

Руководство по mapMulti в Stream API

· 6 мин. чтения

Задача: Наибольшая подстрока палиндром

Для заданной строки s, верните наибольшую подстроку палиндром входящую в s. Подстрока — это непрерывная непустая последовательность символов внутри строки. Стока является палиндромом, если она читается одинаково в обоих направлениях...

ANDROMEDA 42

1. Обзор

В этом руководстве мы рассмотрим метод Stream::mapMulti, представленный в Java 16. Мы напишем простые примеры, иллюстрирующие его использование. В частности, мы увидим, что этот метод похож на Stream:: flatMap . Мы рассмотрим, при каких обстоятельствах мы предпочитаем использовать mapMulti , а не flatMap .

Обязательно ознакомьтесь с нашими статьями о Java Streams , чтобы глубже погрузиться в Stream API.

2. Подпись метода

Опустив подстановочные знаки, метод mapMulti можно записать более кратко:

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

Это промежуточная операция Stream . В качестве параметра требуется реализация функционального интерфейса BiConsumer . Реализация BiConsumer берет элемент Stream T , если необходимо, преобразует его в тип R и вызывает Consumer:: accept преобразователя . ``

Внутри реализации метода mapMulti в Java средство сопоставления представляет собой буфер, реализующий функциональный интерфейс Consumer .

Каждый раз, когда мы вызываем Consumer::accept, он накапливает элементы в буфере и передает их в потоковый конвейер.

3. Простой пример реализации

Давайте рассмотрим список целых чисел для выполнения следующей операции:

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());

В нашей лямбда-реализации BiConsumer<T, Consumer<R>> mapper мы сначала выбираем только четные целые числа, затем добавляем к ним количество, указанное в процентах , приводим результат к типу double и завершаем вызов Consumer.accept .

Как мы видели ранее, потребитель — это просто буфер, который передает возвращаемые элементы в потоковый конвейер. (В качестве примечания обратите внимание, что мы должны использовать свидетель типа <Double>mapMulti для возвращаемого значения, поскольку в противном случае компилятор не сможет определить правильный тип R в сигнатуре метода.)

Это преобразование «один к нулю» или «один к одному» в зависимости от того, является ли элемент нечетным или четным.

Обратите внимание, что оператор if в предыдущем примере кода играет роль Stream::filter , а преобразование целого числа в двойное — роль Stream::map . Следовательно, мы могли бы использовать фильтр и карту Stream для достижения того же результата:

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());

Однако реализация mapMulti более прямолинейна, поскольку нам не нужно вызывать так много промежуточных потоковых операций .

Еще одним преимуществом является то, что реализация mapMulti является обязательной, что дает нам больше свободы для преобразования элементов .

Для поддержки примитивных типов int , long и double у нас есть варианты mapMultiToDouble , mapMultiToInt и mapMultiToLong mapMulti . ``

Например, мы можем использовать mapMultiToDouble , чтобы найти сумму предыдущего списка удвоений:

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. Более реалистичный пример

Рассмотрим коллекцию Album s:

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;
}
// ...
}

В каждом альбоме есть список исполнителей :

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;
}
// ...
}

Если мы хотим собрать список пар имен исполнителей и альбомов, мы можем реализовать это с помощью 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()));
}
})

Для каждого альбома в потоке мы перебираем исполнителей, создаем Apache Commons ImmutablePair имен исполнителей и альбомов и вызываем C onsumer::accept . Реализация mapMulti накапливает элементы, принятые потребителем, и передает их в потоковый конвейер.

Это имеет эффект преобразования «один ко многим», когда результаты накапливаются в потребителе, но в конечном итоге объединяются в новый поток. По сути, это то, что делает Stream::flatMap , чтобы мы могли достичь того же результата со следующей реализацией:

List<Pair<String, String>> artistAlbum = albums.stream()
.flatMap(album -> album.getArtists()
.stream()
.map(artist -> new ImmutablePair<String, String>(artist.getName(), album.getAlbumName())))
.collect(toList());

Мы видим, что оба метода дают одинаковые результаты. Далее мы рассмотрим, в каких случаях более выгодно использовать mapMulti .

5. Когда использовать mapMulti вместо flatMap

5.1. Замена элементов потока небольшим количеством элементов

Как сказано в документации Java: «при замене каждого элемента потока небольшим (возможно, нулевым) количеством элементов. Использование этого метода позволяет избежать накладных расходов на создание нового экземпляра Stream для каждой группы элементов результата, как того требует flatMap».

Давайте напишем простой пример, иллюстрирующий этот сценарий:

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()));
}
}
})

Для каждого альбома мы перебираем исполнителей и накапливаем ноль или несколько пар «исполнитель-альбом», в зависимости от цены альбома по сравнению с переменной upperCost .

Для достижения тех же результатов с помощью 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());

Мы видим, что императивная реализация mapMulti более производительна — нам не нужно создавать промежуточные потоки с каждым обрабатываемым элементом, как это делается при декларативном подходе flatMap .

5.2. Когда легче генерировать элементы результата

Давайте напишем в классе Album метод, который передает потребителю все пары «исполнитель-альбом» со связанными с ними основными лейблами:

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));
}
}
}
// ...
}

Если у исполнителя есть связь с крупными лейблами, реализация объединяет лейблы в строку, разделенную запятыми. Затем он создает пару имен исполнителей и альбомов с метками и вызывает C onsumer::accept .

Если мы хотим получить список всех пар, это так же просто, как использовать mapMulti со ссылкой на метод Album::artistAlbumPairsToMajorLabels :

List<Pair<String, String>> copyrightedArtistAlbum = albums.stream()
.<Pair<String, String>> mapMulti(Album::artistAlbumPairsToMajorLabels)
.collect(toList());

Мы видим, что в более сложных случаях у нас могут быть очень сложные реализации ссылки на метод. Например, в документации по Java приведен пример использования рекурсии.

В общем, воспроизвести те же результаты с помощью flatMap будет очень сложно. Поэтому мы должны использовать mapMulti в тех случаях, когда генерировать элементы результата намного проще, чем возвращать их в виде Stream , как это требуется в flatMap .

6. Заключение

В этом руководстве мы рассмотрели, как реализовать mapMulti на различных примерах. Мы видели, как он сравнивается с flatMap и когда его более выгодно использовать.

В частности, рекомендуется использовать mapMulti , когда необходимо заменить несколько элементов потока или когда проще использовать императивный подход для создания элементов конвейера потока.

Исходный код можно найти на GitHub .