0

Java中将多个Map扁平化为单个Map

 1 month ago
source link: https://www.jdon.com/73081.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Java中将多个Map扁平化为单个Map

自 Java 8 推出以来,处理数据流已成为 Java 开发中的一项常见任务。通常,这些流包含复杂的结构(例如映射),这在进一步处理它们时可能会带来挑战。

在本教程中,我们将探讨如何将地Map映射流展平为单个Map映射。

在深入研究解决方案之前,让我们先澄清一下“展平Map流”的含义。本质上,我们希望将映射流转换为单个Map映射,其中包含流中每个Map映射的所有键值对。

像往常一样,一个例子可以帮助我们快速理解问题。假设我们有三个存储玩家姓名和分数之间关联的Map映射:

Map<String, Integer> playerMap1 = new HashMap<String, Integer>() {{
    put("Kai", 92);
    put("Liam", 100);
}};
Map<String, Integer> playerMap2 = new HashMap<String, Integer>() {{
    put("Eric", 42);
    put("Kevin", 77);
}};
Map<String, Integer> playerMap3 = new HashMap<String, Integer>() {{
    put("Saajan", 35);
}};
我们的输入是包含这些映射的流。为简单起见,我们将在本教程中使用Stream.of(playerMap1, playerMap2 , …)构建输入流。然而,值得注意的是,流不一定具有定义的遇到顺序。

现在,我们的目标是将包含上述三个Map映射的流合并为一个名称-分数Map映射:

Map<String, Integer> expectedMap = new HashMap<String, Integer>() {{
    put("Saajan", 35);
    put("Liam", 100);
    put("Kai", 92);
    put("Eric", 42);
    put("Kevin", 77);
}};
值得一提的是,由于我们使用的是HashMap对象,因此无法保证最终结果中的条目顺序。

此外,流中的Map映射可能包含重复的键和空值。稍后,我们将扩展示例以涵盖本教程中的这些场景。

接下来,让我们深入研究代码。

使用flatMap()和Collectors.toMap()
合并Map的一种方法是使用flatMap()方法和toMap ()收集器:

Map<String, Integer> mergedMap = Stream.of(playerMap1, playerMap2, playerMap3)
  .flatMap(map -> map.entrySet()
    .stream())
  .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
assertEquals(expectedMap, mergedMap);
  • 在上面的代码中,flatMap ()方法将流中的每个映射展平为其条目流。
  • 然后,我们使用toMap()收集器将流的元素收集到单个映射中。

toMap ()收集器需要两个函数作为参数:

  1. 一个用于提取键 ( Map.Entry::getKey ),
  2. 另一个用于提取值 ( Map.Entry::getValue )。

这里,我们使用方法引用来表示这两个函数。这些函数应用于流中的每个条目以构造结果映射。

处理重复键
我们学习了如何使用toMap()收集器将HashMap流合并到一个映射中。然而,如果Map映射流包含重复的键,这种方法将会失败。例如,如果我们将具有重复键“Kai”的新映射添加到流中,则会抛出IllegalStateException:

Map<String, Integer> playerMap4 = new HashMap<String, Integer>() {{
    put("Kai", 76);
}};
assertThrows(IllegalStateException.class, () -> Stream.of(playerMap1, playerMap2, playerMap3, playerMap4)
  .flatMap(map -> map.entrySet()
    .stream())
  .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)), "Duplicate key Kai (attempted merging values 92 and 76)");

为了解决重复键的问题,我们可以将合并函数作为第三个参数传递给toMap()方法来处理与重复键关联的值。

对于重复键场景,我们可能有不同的合并要求。在我们的示例中,一旦出现重复名称,我们希望选择较高的分数。因此,我们的目标是得到这Map:

Map<String, Integer> expectedMap = new HashMap<String, Integer>() {{
    put("Saajan", 35);
    put("Liam", 100);
    put("Kai", 92); // <- max of 92 and 76
    put("Eric", 42);
    put("Kevin", 77);
}};

接下来我们看看如何实现:

Map<String, Integer> mergedMap = Stream.of(playerMap1, playerMap2, playerMap3, playerMap4)
  .flatMap(map -> map.entrySet()
    .stream())
  .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, Integer::max));

assertEquals(expectedMap, mergedMap);

如代码中所示,我们使用方法引用Integer::max作为toMap()中的合并函数。这确保了当出现重复键时,最终映射中的结果值将是与这些键关联的两个值中较大的一个。

处理空值
我们已经看到Collectors.toMap()可以方便地将条目收集到单个映射中。但是,Collectors.toMap ()方法无法将null处理为 map 的值。如果任何映射条目的值为null,我们的解决方案将引发NullPointerException 。

让我们添加一个新Map来验证:

Map<String, Integer> playerMap5 = new HashMap<String, Integer>() {{
    put("Kai", null);
    put("Jerry", null);
}};
assertThrows(NullPointerException.class, () -> Stream.of(playerMap1, playerMap2, playerMap3, playerMap4, playerMap5)
  .flatMap(map -> map.entrySet()
    .stream())
  .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, Integer::max)));

现在,输入流中的映射包含重复的键和空值。这一次,我们仍然希望重复的玩家名字能够获得更高的分数。此外,我们将null视为最低分数。 然后,我们的预期结果如下:

Map<String, Integer> expectedMap = new HashMap<String, Integer>() {{
    put("Saajan", 35);
    put("Liam", 100);
    put("Kai", 92); // <- max of 92, 76, and null
    put("Eric", 42);
    put("Kevin", 77);
    put("Jerry", null);
}};

由于Integer.max()无法处理null值,因此我们创建一个 null 安全方法来从两个可为 null 的Integer对象中获取较大的值:

private Integer maxInteger(Integer int1, Integer int2) {
    if (int1 == null) {
        return int2;
    }
    if (int2 == null) {
        return int1;
    }
    return max(int1, int2);
}

接下来我们来解决这个问题。

使用flatMap()和forEach()
解决这个问题的一个简单方法是首先初始化一个空映射,然后在forEach()中将put()所需的键值对放入其中:

Map<String, Integer> mergedMap = new HashMap<>();
Stream.of(playerMap1, playerMap2, playerMap3, playerMap4, playerMap5)
  .flatMap(map -> map.entrySet()
    .stream())
  .forEach(entry -> {
      String k = entry.getKey();
      Integer v = entry.getValue();
      if (mergedMap.containsKey(k)) {
          mergedMap.put(k, maxInteger(mergedMap.get(k), v));
      } else {
          mergedMap.put(k, v);
      }
    });
assertEquals(expectedMap, mergedMap);

使用groupingBy()、  mapping()和reducing()
flatMap () + forEach()解决方案很简单。然而,它不是一种函数式方法,需要我们编写一些样板合并逻辑。

或者,我们可以结合groupingBy()、mapping()和ducing() 收集器来从功能上解决这个问题:

Map<String, Integer> mergedMap = Stream.of(playerMap1, playerMap2, playerMap3, playerMap4, playerMap5)
  .flatMap(map -> map.entrySet()
    .stream())
  .collect(groupingBy(Map.Entry::getKey, mapping(Map.Entry::getValue, reducing(null, this::maxInteger))));

assertEquals(expectedMap, mergedMap);

如上面的代码所示,我们在collect()方法中组合了三个收集器。接下来,让我们快速了解一下他们是如何协同工作的:

  • groupingBy(Map.Entry::getKey, mapping(…)) –按键对映射条目进行分组以获取键 -> 条目结构,这些条目将转到映射()
  • Map(Map.Entry :: getValue,reducing(…) -使用Map.Entry :: getValue将每个 Entry映射到Integer并将Integer值移交给另一个下游收集器reducing()的下游收集器
  • reduce(null, this::maxInteger) – 下游收集器通过执行maxInteger函数来应用减少重复键的整数值的逻辑,该函数返回两个整数值中的最大值

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK