如何根据 Java 8 个流的值合并两个映射?

How to merge two Maps based on values with Java 8 streams?

我有 CollectionMap 包含库存信息:

 0 
  "subtype" -> "DAIRY"
  "itemNumber" -> "EU999"
  "quantity" -> "60"
 1 
  "subtype" -> "DAIRY"
  "itemNumber" -> "EU999"
  "quantity" -> "1000"
 2 
  "subtype" -> "FRESH"
  "itemNumber" -> "EU999"
  "quantity" -> "800"
 3
  "subtype" -> "FRESH"
  "itemNumber" -> "EU100"
  "quantity" -> "100"

我需要根据 itemNumber 压缩此列表,同时对 quantity 求和并在逗号分隔的字符串中保留唯一的 subtypes。意思是,new Maps 看起来像这样:

 0 
  "subtype" -> "DAIRY, FRESH"
  "itemNumber" -> "EU999"
  "quantity" -> "1860"
 1 
  "subtype" -> "FRESH"
  "itemNumber" -> "EU100"
  "quantity" -> "100"

我尝试了各种流、收集器、groupby 等,但我迷路了。

这是我目前拥有的:

public Collection<Map> mergeInventoryPerItemNumber(Collection<Map> InventoryMap){
        Map condensedInventory = null;
        InventoryMap.stream()
                .collect(groupingBy(inv -> new ImmutablePair<>(inv.get("itemNumber"), inv.get("subtype")))), collectingAndThen(toList(), list -> {
            long count = list.stream()
                    .map(list.get(Integer.parseInt("quantity")))
                    .collect(counting());
            String itemNumbers = list.stream()
                    .map(list.get("subtype"))
                    .collect(joining(" , "));
            condensedInventory.put("quantity", count);
            condensedInventory.put("subtype", itemNumbers);

            return condensedInventory;
        });

也许一次扫描就可以做到这一点,但在这里我通过两次解决了这个问题:一次将相似的项目组合在一起,另一次在每个组中的项目上构建一个代表性项目(这似乎在精神上与您的代码相似,您也试图从组中流式传输元素)。

   
    public static Collection<Map<String, String>> 
            mergeInventoryPerItemNumber(Collection<Map<String, String>> m){

        return m.stream()
                // returns a map of itemNumber -> list of products with that number
                .collect(Collectors.groupingBy(o -> o.get("itemNumber")))
                // for each item number, builds new representative product
                .entrySet().stream().map(e -> Map.of(
                    "itemNumber", e.getKey(), 
                    // ... merging non-duplicate subtypes
                    "subtype", e.getValue().stream()
                        .map(v -> v.get("subtype"))
                        .distinct() // avoid duplicates
                        .collect(Collectors.joining(", ")), 
                    // ... adding up quantities
                    "quantity", ""+e.getValue().stream()
                        .map(v -> Integer.parseInt(v.get("quantity")))
                        .reduce(0, Integer::sum)))
                .collect(Collectors.toList());
    }

    public static void main(String ... args) {
        Collection<Map<String, String>> c = mkMap();
        dump(c);
        dump(mergeInventoryPerItemNumber(c));
    }

    public static Collection<Map<String, String>> mkMap() {
        return List.of(
            Map.of("subtype", "DAIRY", "itemNumber", "EU999", "quantity", "60"),
            Map.of("subtype", "DAIRY", "itemNumber", "EU999", "quantity", "1000"),
            Map.of("subtype", "FRESH", "itemNumber", "EU999", "quantity", "800"),
            Map.of("subtype", "FRESH", "itemNumber", "EU100", "quantity", "100"));
    }

    public static void dump(Collection<Map<String, String>> col) {
        int i = 0;
        for (Map<String, String> m : col) {
            System.out.println(i++);
            for (Map.Entry e : m.entrySet()) {
                System.out.println("\t" + e.getKey() + " -> " + e.getValue());
            }
        }
    }

您在这里误用了 Map。每个地图都包含相同的键(“subtype”、“itemNumber”、“quantity”)。在您的代码中,它们几乎被视为对象属性。它们应该出现在每个地图中,并且每个地图都应该具有特定范围的值,尽管根据您的示例存储为字符串。

Side-note: 避免使用行类型(如 Map 没有尖括号 <> 中的通用信息),在这种情况下,集合中的所有元素都将被视为 Objects.

Item 显然必须定义为 class。通过将这些数据存储在 map 中,您将失去为每个 [=112] 定义 适当数据类型 的可能性=],而且您无法 定义行为 来使用这些 属性for更详细的解释请看 ).

public class Item {
    private final String itemNumber;
    private Set<Subtype> subtypes;
    private long quantity;

    public Item combine(Item other) {
        Set<Subtype> combinedSubtypes = new HashSet<>(subtypes);
        combinedSubtypes.addAll(other.subtypes);

        return new Item(this.itemNumber,
                        combinedSubtypes,
                        this.quantity + other.quantity);
    }

    // + constructor, getters, hashCode/equals, toString
}

方法combine表示将两个项目合并在一起的逻辑。通过将它放在这个 class 中,您可以轻松地重用它并在需要时更改它。

subtype 字段类型的最佳选择是 enum。因为它可以 避免错误 由拼写错误的字符串值引起,而且 enums 有广泛的语言支持(switch 表达式语句,特别为枚举设计的特殊数据结构enum 可以与 annotations) 一起使用。

这个自定义 enum 可以像这样。

public enum Subtype {DAIRY, FRESH}

通过所有这些更改,mergeInventoryPerItemNumber() 中的代码变得简洁易懂。 Collectors.groupingBy() 用于通过将 与相同的 itemNumber 分组来创建地图。下游收集器 Collectors.reducing() 用于将分组在同一 key 下的 items 组合为 单个对象 .

请注意 Collectors.reducing() 会产生 Optional 结果。因此,filter(Optional::isPresent) 用作预防措施,以确保结果存在,后续操作 map(Optional::get) 可选对象 item 中提取 item =81=].

public static Collection<Item> mergeInventoryPerItemNumber(Collection<Item> inventory) {
    return inventory.stream()
            .collect(Collectors.groupingBy(Item::getItemNumber,
                            Collectors.reducing(Item::combine)))
            .values().stream()
            .filter(Optional::isPresent)
            .map(Optional::get)
            .collect(Collectors.toList());
}

main()

public static void main(String[] args) {
    List<Item> inventory =
            List.of(new Item("EU999", Set.of(Subtype.DAIRY), 60),
                    new Item("EU999", Set.of(Subtype.DAIRY), 1000),
                    new Item("EU999", Set.of(Subtype.FRESH), 800),
                    new Item("EU100", Set.of(Subtype.FRESH), 100));

    Collection<Item> combinedItems = mergeInventoryPerItemNumber(inventory);

    combinedItems.forEach(System.out::println);
}

输出

Item{itemNumber='EU100', subtypes=[FRESH], quantity=100}
Item{itemNumber='EU999', subtypes=[FRESH, DAIRY], quantity=1860}

这是一种方法。

  • 首先遍历地图列表。
  • 对于每个地图,根据需要处理键
    • 特殊键是 itemNumberquantity
    • itemNumber 是所有值的连接元素。
    • quantity是必须作为整数对待的值
    • 其他是字符串并被视为字符串(对于所有其他值,如果该值已存在于连接值的字符串中,则不会再次添加)

一些数据

List<Map<String, String>> mapList = List.of(
        Map.of("subtype", "DAIRY", "itemNumber", "EU999",
                "quantity", "60"),
        Map.of("subtype", "DAIRY", "itemNumber", "EU999",
                "quantity", "1000"),
        Map.of("subtype", "FRESH", "itemNumber", "EU999",
                "quantity", "800"),
        Map.of("subtype", "FRESH", "itemNumber", "EU100",
                "quantity", "100"));

构建过程

Map<String, Map<String, String>> result = new HashMap<>();

for (Map<String, String> m : mapList) {
    result.compute(m.get("itemNumber"), (k, v) -> {
        for (Entry<String, String> e : m.entrySet()) {
            String key = e.getKey();
            String value = e.getValue();
            if (v == null) {
                v = new HashMap<String, String>();
                v.put(key, value);
            } else {
                if (key.equals("quantity")) {
                    v.compute(key,
                            (kk, vv) -> vv == null ? value :
                                    Integer.toString(Integer
                                            .valueOf(vv)
                                            + Integer.valueOf(
                                                    value)));
                } else {
                    v.compute(key, (kk, vv) -> vv == null ?
                            value : (vv.contains(value) ? vv :
                                    vv + ", " + value));
                }
            }
        }
        return v;
    });
}

List<Map<String,String>> list = new ArrayList<>(result.values());
        
for (int i = 0; i < list.size(); i++) {
  System.out.println(i + " " + list.get(i));
}

打印

0 {itemNumber=EU100, quantity=100, subtype=FRESH}
1 {itemNumber=EU999, quantity=1860, subtype=DAIRY, FRESH}

请注意,地图中的地图可能比地图列表更有用。例如,您可以通过简单地指定所需的键来检索 itemNumber 的映射。

System.out.println(result.get("EU999"));

打印

{itemNumber=EU999, quantity=1860, subtype=DAIRY, FRESH}