Java Streams: объединение двух коллекций в карту
у меня есть две коллекции, список складских идентификаторов и коллекция виджетов. Виджеты существуют на нескольких складах в разных количествах:
List<Long> warehouseIds;
List<Widget> widgets;
вот пример defeinition классов:
public class Widget {
public Collection<Stock> getStocks();
}
public class Stock {
public Long getWarehouseId();
public Integer getQuantity();
}
Я хочу использовать API Streams для создания карты, где идентификатор склада является ключевым, а значение-это список виджетов с наименьшим количеством на определенном складе. Поскольку несколько виджетов могут иметь одинаковое количество, мы возвращаем список.
например, склад 111 и 5 кол-во Виджет A, 5 of Виджет B и 8 of Виджет C.
склад 222 и 0 кол-во Виджет A, 5 of Виджет B и 5 of Виджет C Возвращаемая карта будет иметь следующее записи:
111 => ['WidgetA', 'WidgetB']
222 => ['WidgetA']
запуск настройки карты с ключами кажется довольно простым, но я не знаю, как структурировать уменьшение вниз по течению:
warehouseIds.stream().collect(Collectors.groupingBy(
Function::Identity,
HashMap::new,
???...
Я думаю, что проблема, с которой я сталкиваюсь, заключается в уменьшении виджетов на основе идентификатора склада и не зная, как вернуть коллектор для создания этого списка виджетов. Вот как я в настоящее время получаю список виджетов с наименьшим запасом на определенном складе (представленном by someWarehouseId):
widgets.stream().collect(Collectors.groupingBy(
(Widget w)->
w.getStocks()
//for a specific warehouse
.stream().filter(stock->stock.getWarehouseId()==someWarehouseId)
//Get the quantity of stocks for a widget
.collect(Collectors.summingInt(Stock::getQuantity)),
//Use a tree map so the keys are sorted
TreeMap::new,
//Get the first entry
Collectors.toList())).firstEntry().getValue();
разделение этого на две задачи с использованием forEach в списке складов облегчит эту работу, но мне интересно, смогу ли я сделать это в "однострочном".
1 ответов
чтобы решить эту проблему, нам нужно использовать более правильный подход, чем использование TreeMap
для выбора значений, имеющих наименьшие величины.
рассмотрим следующий подход:
- мы
Stream<Widget>
наши первые виджеты. Нам нужно будет выполнить некоторую обработку запасов каждого виджета,но нам также нужно будет сохранить виджет. Давайте!--4--> этоStream<Widget>
наStream<Map.Entry<Stock, Widget>>
: что новый поток будет состоять из каждогоStock
что у нас есть, с тегомWidget
. - мы фильтруем эти элементы, чтобы только сохранить
Map.Entry<Stock, Widget>
где запас имеетwarehouseId
, содержащиеся вwarehouseIds
список. - теперь нам нужно сгруппировать этот поток в соответствии с
warehouseId
каждогоStock
. Поэтому мы используемCollectors.groupingBy(classifier, downstream)
где классификатор возвращает этоwarehouseId
. - нисходящий коллектор собирает элементы, которые классифицируются на один и тот же ключ. В этом случае, для
Map.Entry<Stock, Widget>
элементы, которые были классифицируется так жеwarehouseId
, нам нужно держать только те где запас имеет самое низкое количество. Для этого нет встроенных коллекторов, давайте использоватьMoreCollectors.minAll(comparator, downstream)
С StreamEx библиотека. Если вы предпочитаете не использовать библиотеку, я извлек ее код в этот ответ и буду использовать его. - компаратор просто сравнивает количество каждого запаса в
Map.Entry<Stock, Widget>
. Это гарантирует, что мы сохраним элементы с наименьшим количеством для фиксированногоwarehouseId
. Нисходящий сборник использован для уменьшения собранных элементов. В этом случае мы хотим только сохранить виджет, поэтому мы используемCollectors.mapping(mapper, downstream)
где картограф возвращает виджет изMap.Entry<Stock, Widget>
и нижестоящие коллекторы собираются в список сCollectors.toList()
.
пример кода:
Map<Long, List<Widget>> map =
widgets.stream()
.flatMap(w -> w.getStocks().stream().map(s -> new AbstractMap.SimpleEntry<>(s, w)))
.filter(e -> warehouseIds.contains(e.getKey().getWarehouseId()))
.collect(Collectors.groupingBy(
e -> e.getKey().getWarehouseId(),
minAll(
Comparator.comparingInt(e -> e.getKey().getQuantity()),
Collectors.mapping(e -> e.getValue(), Collectors.toList())
)
));
следующим minAll
взыскатель:
public static <T, A, D> Collector<T, ?, D> minAll(Comparator<? super T> comparator, Collector<T, A, D> downstream) {
return maxAll(comparator.reversed(), downstream);
}
public static <T, A, D> Collector<T, ?, D> maxAll(Comparator<? super T> comparator, Collector<? super T, A, D> downstream) {
final class PairBox<U, V> {
public U a;
public V b;
PairBox(U a, V b) {
this.a = a;
this.b = b;
}
}
Supplier<A> downstreamSupplier = downstream.supplier();
BiConsumer<A, ? super T> downstreamAccumulator = downstream.accumulator();
BinaryOperator<A> downstreamCombiner = downstream.combiner();
Supplier<PairBox<A, T>> supplier = () -> new PairBox<>(downstreamSupplier.get(), null);
BiConsumer<PairBox<A, T>, T> accumulator = (acc, t) -> {
if (acc.b == null) {
downstreamAccumulator.accept(acc.a, t);
acc.b = t;
} else {
int cmp = comparator.compare(t, acc.b);
if (cmp > 0) {
acc.a = downstreamSupplier.get();
acc.b = t;
}
if (cmp >= 0)
downstreamAccumulator.accept(acc.a, t);
}
};
BinaryOperator<PairBox<A, T>> combiner = (acc1, acc2) -> {
if (acc2.b == null) {
return acc1;
}
if (acc1.b == null) {
return acc2;
}
int cmp = comparator.compare(acc1.b, acc2.b);
if (cmp > 0) {
return acc1;
}
if (cmp < 0) {
return acc2;
}
acc1.a = downstreamCombiner.apply(acc1.a, acc2.a);
return acc1;
};
Function<PairBox<A, T>, D> finisher = acc -> downstream.finisher().apply(acc.a);
return Collector.of(supplier, accumulator, combiner, finisher);
}