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 ()方法将流中的每个映射展平为其条目流。
- 然后,我们使用toMap()收集器将流的元素收集到单个映射中。
toMap ()收集器需要两个函数作为参数:
- 一个用于提取键 ( Map.Entry::getKey ),
- 另一个用于提取值 ( Map.Entry::getValue )。
这里,我们使用方法引用来表示这两个函数。这些函数应用于流中的每个条目以构造结果映射。
处理重复键
我们学习了如何使用toMap()收集器将HashMap流合并到一个映射中。然而,如果Map映射流包含重复的键,这种方法将会失败。例如,如果我们将具有重复键“Kai”的新映射添加到流中,则会抛出IllegalStateException:
Map<String, Integer> playerMap4 = new HashMap<String, Integer>() {{ |
为了解决重复键的问题,我们可以将合并函数作为第三个参数传递给toMap()方法来处理与重复键关联的值。
对于重复键场景,我们可能有不同的合并要求。在我们的示例中,一旦出现重复名称,我们希望选择较高的分数。因此,我们的目标是得到这Map:
Map<String, Integer> expectedMap = new HashMap<String, Integer>() {{ |
接下来我们看看如何实现:
Map<String, Integer> mergedMap = Stream.of(playerMap1, playerMap2, playerMap3, playerMap4) |
如代码中所示,我们使用方法引用Integer::max作为toMap()中的合并函数。这确保了当出现重复键时,最终映射中的结果值将是与这些键关联的两个值中较大的一个。
处理空值
我们已经看到Collectors.toMap()可以方便地将条目收集到单个映射中。但是,Collectors.toMap ()方法无法将null处理为 map 的值。如果任何映射条目的值为null,我们的解决方案将引发NullPointerException 。
让我们添加一个新Map来验证:
Map<String, Integer> playerMap5 = new HashMap<String, Integer>() {{ |
现在,输入流中的映射包含重复的键和空值。这一次,我们仍然希望重复的玩家名字能够获得更高的分数。此外,我们将null视为最低分数。 然后,我们的预期结果如下:
Map<String, Integer> expectedMap = new HashMap<String, Integer>() {{ |
由于Integer.max()无法处理null值,因此我们创建一个 null 安全方法来从两个可为 null 的Integer对象中获取较大的值:
private Integer maxInteger(Integer int1, Integer int2) { |
接下来我们来解决这个问题。
使用flatMap()和forEach()
解决这个问题的一个简单方法是首先初始化一个空映射,然后在forEach()中将put()所需的键值对放入其中:
Map<String, Integer> mergedMap = new HashMap<>(); |
使用groupingBy()、 mapping()和reducing()
flatMap () + forEach()解决方案很简单。然而,它不是一种函数式方法,需要我们编写一些样板合并逻辑。
或者,我们可以结合groupingBy()、mapping()和ducing() 收集器来从功能上解决这个问题:
Map<String, Integer> mergedMap = Stream.of(playerMap1, playerMap2, playerMap3, playerMap4, playerMap5) |
如上面的代码所示,我们在collect()方法中组合了三个收集器。接下来,让我们快速了解一下他们是如何协同工作的:
- groupingBy(Map.Entry::getKey, mapping(…)) –按键对映射条目进行分组以获取键 -> 条目结构,这些条目将转到映射()
- Map(Map.Entry :: getValue,reducing(…) -使用Map.Entry :: getValue将每个 Entry映射到Integer并将Integer值移交给另一个下游收集器reducing()的下游收集器
- reduce(null, this::maxInteger) – 下游收集器通过执行maxInteger函数来应用减少重复键的整数值的逻辑,该函数返回两个整数值中的最大值