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