Stream::mapMulti
- это новый метод, который классифицируется как промежуточная операция.
Требуется BiConsumer<T, Consumer<R>> mapper
элемента, который должен быть обработан, Consumer
. Последнее делает этот метод странным на первый взгляд, потому что он отличается от того, к чему мы привыкли в других промежуточных методах, таких как map
, filter
или peek
, где ни один из них не использует никаких вариаций *Consumer
.
Цель Consumer
, предоставленного прямо в лямбда-выражении самим API, - принять любые числовые элементы, которые будут доступны в последующем конвейере. Следовательно, все элементы, независимо от того, сколько их, будут размножены.
Объяснение с использованием простых фрагментов
Сопоставление "один с некоторым" (0..1) (аналогично filter
)
Использование consumer.accept(R r)
только для нескольких выбранных элементов позволяет получить конвейер, подобный фильтру. Это может быть полезно в случае проверки элемента на соответствие предикату и его сопоставления с другим значением, что в противном случае было бы сделано с использованием комбинации filter
и map
. Следующий
Stream.of("Java", "Python", "JavaScript", "C#", "Ruby")
.mapMulti((str, consumer) -> {
if (str.length() > 4) {
consumer.accept(str.length()); // lengths larger than 4
}
})
.forEach(i -> System.out.print(i + " "));
// 6 10
Сопоставление один-к-одному (аналогично map
)
Работая с предыдущим примером, когда условие опущено и каждый элемент отображается в новый и принимается с использованием consumer
, метод эффективно ведет себя как map
:
Stream.of("Java", "Python", "JavaScript", "C#", "Ruby")
.mapMulti((str, consumer) -> consumer.accept(str.length()))
.forEach(i -> System.out.print(i + " "));
// 4 6 10 2 4
Сопоставление "один ко многим" (аналогично flatMap
)
Здесь все становится интересно, потому что можно вызывать consumer.accept(R r)
любое количество раз. Допустим, мы хотим воспроизвести число, представляющее длину строки, то есть 2
становится 2
, 2
. 4
становится 4
, 4
, 4
, 4
. и 0
ничего не становится.
Stream.of("Java", "Python", "JavaScript", "C#", "Ruby", "")
.mapMulti((str, consumer) -> {
for (int i = 0; i < str.length(); i++) {
consumer.accept(str.length());
}
})
.forEach(i -> System.out.print(i + " "));
// 4 4 4 4 6 6 6 6 6 6 10 10 10 10 10 10 10 10 10 10 2 2 4 4 4 4
Сравнение с flatMap
Сама идея этого механизма заключается в том, что его можно вызывать несколько раз (включая ноль), а его использование SpinedBuffer
внутри позволяет проталкивать элементы в один сплющенный экземпляр Stream без создания нового для каждой группы. элементов вывода в отличие от flatMap
. JavaDoc указывает два варианта использования, когда использование этого метода предпочтительнее flatMap
:
- При замене каждого элемента потока небольшим (возможно, нулевым) количеством элементов. Использование этого метода позволяет избежать накладных расходов на создание нового экземпляра Stream для каждой группы элементов результата, как того требует flatMap.
- Когда проще использовать императивный подход для генерации элементов результата, чем возвращать их в форме Stream.
С точки зрения производительности новый метод mapMulti
является лучшим в таких случаях. Посмотрите тест внизу этого ответа.
Сценарий фильтра-карты
Использование этого метода вместо filter
или map
по отдельности не имеет смысла из-за его многословности и того факта, что в любом случае создается один промежуточный поток. Исключением может быть замена цепочки .filter(..).map(..)
вызываемых вместе, что удобно в таких случаях, как проверка типа элемента и его приведение.
int sum = Stream.of(1, 2.0, 3.0, 4F, 5, 6L)
.mapMultiToInt((number, consumer) -> {
if (number instanceof Integer) {
consumer.accept((Integer) number);
}
})
.sum();
// 6
int sum = Stream.of(1, 2.0, 3.0, 4F, 5, 6L)
.filter(number -> number instanceof Integer)
.mapToInt(number -> (Integer) number)
.sum();
Как видно выше, его варианты вроде _ 39_, _ 40_ и _ 41_. Это входит в mapMulti
методы в примитивных потоках, таких как _ 43_. Также были введены три новых функциональных интерфейса. По сути, это примитивные варианты BiConsumer<T, Consumer<R>>
, например:
@FunctionalInterface
interface IntMapMultiConsumer {
void accept(int value, IntConsumer ic);
}
Комбинированный реальный сценарий использования
Настоящая сила этого метода заключается в его гибкости использования и создании только одного потока за раз, что является основным преимуществом перед flatMap
. Два нижеприведенных фрагмента представляют собой плоское отображение Product
и его List<Variation>
в 0..n
предложений, представленных классом Offer
и основанных на определенных условиях (категория продукта и наличие вариантов).
Product
с String name
, int basePrice
, String category
и List<Variation> variations
.
Variation
с String name
, int price
и boolean availability
.
List<Product> products = ...
List<Offer> offers = products.stream()
.mapMulti((product, consumer) -> {
if ("PRODUCT_CATEGORY".equals(product.getCategory())) {
for (Variation v : product.getVariations()) {
if (v.isAvailable()) {
Offer offer = new Offer(
product.getName() + "_" + v.getName(),
product.getBasePrice() + v.getPrice());
consumer.accept(offer);
}
}
}
})
.collect(Collectors.toList());
List<Product> products = ...
List<Offer> offers = products.stream()
.filter(product -> "PRODUCT_CATEGORY".equals(product.getCategory()))
.flatMap(product -> product.getVariations().stream()
.filter(Variation::isAvailable)
.map(v -> new Offer(
product.getName() + "_" + v.getName(),
product.getBasePrice() + v.getPrice()
))
)
.collect(Collectors.toList());
Использование mapMulti
является более императивным по сравнению с декларативным подходом комбинации методов Stream предыдущих версий, показанной в последнем фрагменте с использованием flatMap
, map
и filter
. С этой точки зрения от варианта использования зависит, проще ли использовать императивный подход. Рекурсия - хороший пример, описанный в JavaDoc.
Контрольный показатель
Как и обещал, я написал кучу микротестов на основе идей, собранных из комментариев. Поскольку для публикации достаточно много кода, я создал репозиторий GitHub. с подробностями реализации, и я собираюсь поделиться только результатами.
Stream::flatMap(Function)
против Stream::mapMulti(BiConsumer)
Источник
Здесь мы видим огромную разницу и доказательство того, что новый метод действительно работает, как описано, и его использование позволяет избежать накладных расходов на создание нового экземпляра Stream с каждым обработанным элементом.
Benchmark Mode Cnt Score Error Units
MapMulti_FlatMap.flatMap avgt 25 73.852 ± 3.433 ns/op
MapMulti_FlatMap.mapMulti avgt 25 17.495 ± 0.476 ns/op
Stream::filter(Predicate).map(Function)
против Stream::mapMulti(BiConsumer)
Источник
Использование цепочечных конвейеров (хотя и не вложенных) - это нормально.
Benchmark Mode Cnt Score Error Units
MapMulti_FilterMap.filterMap avgt 25 7.973 ± 0.378 ns/op
MapMulti_FilterMap.mapMulti avgt 25 7.765 ± 0.633 ns/op
Stream::flatMap(Function)
с Optional::stream()
против Stream::mapMulti(BiConsumer)
Источник
Это очень интересно, особенно с точки зрения использования (см. Исходный код): теперь мы можем сглаживать, используя mapMulti(Optional::ifPresent)
, и, как и ожидалось, новый метод в этом случае работает немного быстрее.
Benchmark Mode Cnt Score Error Units
MapMulti_FlatMap_Optional.flatMap avgt 25 20.186 ± 1.305 ns/op
MapMulti_FlatMap_Optional.mapMulti avgt 25 10.498 ± 0.403 ns/op
person
Nikolas Charalambidis
schedule
30.09.2020
flatMap
, но у них, должно быть, были причины не перегрузить. Тогда я рад, что я не единственный, кто плохо называет API. Добавьте это в закладки, чтобы попробовать и понять, что помешало им улучшить производительность с помощью существующей комбинацииfilter.map
илиflatMap
вместо предоставления нового API. В любом случае спасибо за то, что поделились. - person Naman   schedule 30.09.2020filter.map
противmapMulti
. - person Nikolas Charalambidis   schedule 30.09.2020instanceof
- person fps   schedule 30.09.2020