1. 概述
自Java 8引入流式处理以来,处理数据流已成为常见操作。这些流中常包含Map等复杂结构,给后续处理带来挑战。
本文将探讨如何将Map流扁平化为单个Map,合并所有键值对。
2. 问题引入
首先明确"扁平化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);
}};
输入流包含这些Map(本文使用Stream.of()
构建)。注意:流没有固定的遍历顺序。
目标是将包含三个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可能包含重复键和null值,后续将扩展示例覆盖这些场景。
3. 使用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()
将流中的每个Map扁平化为条目流
✅ toMap()
收集器将条目流收集为单个Map
✅ 使用方法引用提取键(Map.Entry::getKey
)和值(Map.Entry::getValue
)
4. 处理重复键
前述方法在遇到重复键时会失败。例如添加包含重复键"Kai"的新Map:
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)");
⚠️ 抛出IllegalStateException
:重复键Kai(尝试合并值92和76)
解决方案:为toMap()
传递合并函数作为第三个参数,处理重复键的值。本例要求保留较高分数:
Map<String, Integer> expectedMap = new HashMap<String, Integer>() {{
put("Saajan", 35);
put("Liam", 100);
put("Kai", 92); // <- 92和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
作为合并函数
✅ 重复键时保留较大值
5. 处理null值
Collectors.toMap()
无法处理值为null
的情况,会抛出NullPointerException
。验证如下:
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)));
❌ 抛出NullPointerException
需求:
- 重复键仍保留较高分数
- 将
null
视为最低分数
期望结果:
Map<String, Integer> expectedMap = new HashMap<String, Integer>() {{
put("Saajan", 35);
put("Liam", 100);
put("Kai", 92); // <- 92, 76和null中的最大值
put("Eric", 42);
put("Kevin", 77);
put("Jerry", null);
}};
由于Integer.max()
无法处理null
,创建空安全方法:
private Integer maxInteger(Integer int1, Integer int2) {
if (int1 == null) {
return int2;
}
if (int2 == null) {
return int1;
}
return max(int1, int2);
}
5.1. 使用flatMap和forEach
简单粗暴的解决方案:初始化空Map,用forEach()
合并键值对:
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);
✅ 直观易理解
❌ 非函数式风格,需手写合并逻辑
5.2. 使用groupingBy、mapping和reducing
函数式解决方案:组合三个收集器:
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);
工作原理:
groupingBy(Map.Entry::getKey, mapping(...))
→ 按键分组,形成key -> Entries
结构,将Entries传递给mapping()
mapping(Map.Entry::getValue, reducing(...))
→ 下游收集器:将每个Entry映射为Integer,传递给reducing()
reducing(null, this::maxInteger)
→ 下游收集器:对重复键的值执行maxInteger
函数,返回最大值
✅ 纯函数式风格
✅ 无需手写合并逻辑
6. 将Map列表转换为单个Map
在Java中,将Map列表合并为单个Map有多种方法。注意:这些方法会覆盖重复键的条目。
6.1. 使用简单for循环
传统方法:遍历列表,用putAll()
合并:
public static <K, V> Map<K, V> mergeMapsUsingLoop(List<Map<K, V>> listOfMaps) {
Map<K, V> result = new HashMap<>();
for (Map<K, V> map : listOfMaps) {
result.putAll(map);
}
return result;
}
✅ 直接控制迭代和合并逻辑
✅ 代码清晰易读
6.2. 使用Java Streams
Java 8+的流式处理方案:
public static <K, V> Map<K, V> mergeMapsUsingStream(List<Map<K, V>> listOfMaps) {
return listOfMaps.stream()
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (v1, v2) -> v2));
}
✅ 函数式风格
✅ 简洁优雅
✅ 重复键时保留后出现的值((v1, v2) -> v2
)
7. 结论
本文探讨了在Java中合并Map流的多种方法,包括:
- 基础合并方案
- 处理重复键的技巧
- 安全处理null值的策略
- Map列表的转换方法
不同场景可灵活选择合适方案,完整示例代码可在GitHub获取。