Java threads: merging two collections into a map

I have two collections, a list of store IDs and a collection of widgets. Widgets exist in several warehouses in different quantities:

List<Long> warehouseIds; List<Widget> widgets; 

Here is an example of class exclusion:

 public class Widget { public Collection<Stock> getStocks(); } public class Stock { public Long getWarehouseId(); public Integer getQuantity(); } 

I want to use the Streams API to create a Map, where the warehouse key is the key, and the value is a list of widgets with the least quantity in a specific warehouse. Since multiple widgets can have the same number, we return a list.

For example, Warehouse 111 has 5 Widget A numbers, 5 Widget B and 8 Widget C.

Warehouse 222 has 0 number Widget A , 5 Widget B > and 5 Widget C The returned Map would have the following entries:

111 => ['WidgetA', 'WidgetB']

222 => ['WidgetA']

Starting the map setup with the keys seems pretty simple, but I donโ€™t know how to structure the following shortcut:

 warehouseIds.stream().collect(Collectors.groupingBy( Function::Identity, HashMap::new, ???... 

I think that the problem I am running into brings Widgets based on the warehouse ID and does not know how to return Collector to create this list of widgets. Here, as I currently get the list of widgets with the least stock in a specific warehouse (provided 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(); 

Dividing this into two tasks using forEach in the warehouse list will make this work easier, but I wonder if I can do this in a โ€œsingle layerโ€.

+6
source share
1 answer

To solve this problem, we need to use a more correct approach than using TreeMap to select the values โ€‹โ€‹with the smallest values.

Consider the following approach:

  • We make the Stream<Widget> our original widgets. We will need to process the stocks of each widget, but we will also need to maintain the widget. Let flatMap that Stream<Widget> in Stream<Map.Entry<Stock, Widget>> : the new stream will consist of every Stock that we have, with its corresponding Widget .
  • We filter these items only for storage Map.Entry<Stock, Widget> , where the stock has a warehouseId contained in the warehouseIds list.
  • Now we need to group this stream according to the warehouseId each Stock . Therefore, we use Collectors.groupingBy(classifier, downstream) , where the classifier returns this warehouseId .
  • The downstream collector collects items that are classified into the same key. In this case, for the Map.Entry<Stock, Widget> elements that were assigned to the same warehouseId , we need to save only those where the stock has the smallest amount. There are no built-in collectors for this, use MoreCollectors.minAll(comparator, downstream) from StreamEx . If you prefer not to use the library, I have extracted its code in this answer and will use it.
  • The comparator simply compares the amount of each stock in Map.Entry<Stock, Widget> . This ensures that we keep the items with the least amount for a fixed warehouseId . The lower collector is used to reduce assembled items. In this case, we want to save only the widget, so we use Collectors.mapping(mapper, downstream) , where mapper returns the widgets from Map.Entry<Stock, Widget> and the downstream collectors are collected into a list with Collectors.toList() .

Code example:

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

with the following minAll collector:

 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); } 
+4
source

All Articles