1. 概述
在使用Java流(Stream)生成Map
时,可能会遇到重复的键。这可能会导致在向地图添加值时,与键关联的旧值被覆盖,从而引发问题。
本教程将讨论如何在使用流API生成Map
时处理重复键。
2. 问题介绍
如常,我们通过示例来理解这个问题。假设我们有一个City
类:
class City {
private String name;
private String locatedIn;
public City(String name, String locatedIn) {
this.name = name;
this.locatedIn = locatedIn;
}
// Omitted getter methods
// Omitted the equals() and hashCode() methods
// ...
}
如上代码所示,City
是一个POJO类,有两个字符串属性:城市名称和位置信息。这两个属性在equals()
和hashCode()
方法中都进行了检查。为了简化,这里没有提供方法实现。
接下来,我们创建一个City
实例的列表:
final List<City> CITY_INPUT = Arrays.asList(
new City("New York City", "USA"),
new City("Shanghai", "China"),
new City("Hamburg", "Germany"),
new City("Paris", "France"),
new City("Paris", "Texas, USA"));
如上代码所示,我们从数组初始化了一个List<City>
,以兼容较旧的Java版本。CITY_INPUT
列表包含五个城市。请注意我们放入列表的最后两个城市:
-
new City("Paris", "France")
-
new City("Paris", "Texas, USA")
两个城市的名字都是"Paris",但它们的位置信息不同,表明这两个Paris
实例代表不同的城市。
现在,假设我们想从CITY_INPUT
列表中使用城市名称作为键,生成一个Map
。显然,这两个Paris
城市将拥有相同的键。
接下来,我们将探讨如何在使用Java流API生成地图时处理重复键。
为了简单起见,我们将使用单元测试断言来验证每个解决方案是否产生预期的结果。
3. 使用groupingBy()
方法生成Map<Key, List<Value>>
处理地图中重复键的一个想法是让键关联到集合中的多个值,例如Map<String, List<City>>
。一些流行库(如Guava的Multimap
和Apache Commons的MultiValuedMap
)提供了更方便处理多值地图的类型。
在这个教程中,我们将坚持使用标准Java API。因此,我们将使用groupingBy()
收集器生成一个Map<String, List<City>>
结果,因为groupingBy()
方法可以根据某些属性对对象进行分组,并将它们存储在Map
实例中:
Map<String, List<City>> resultMap = CITY_INPUT.stream()
.collect(groupingBy(City::getName));
assertEquals(4, resultMap.size());
assertEquals(Arrays.asList(new City("Paris", "France"), new City("Paris", "Texas, USA")),
resultMap.get("Paris"));
如上测试所示,groupingBy()
收集器产生的地图结果包含四个条目。同时,两个名为"Paris"的城市实例被归入"Paris"键下。
因此,使用多值映射的方法可以解决键重复的问题。但是,这种方法返回的是Map<String, List<City>>
。如果我们需要Map<String, City>
类型的返回值,我们就不能再将具有重复键的对象一起存储在集合中。
那么,接下来我们将探讨在这种情况下如何处理重复键。
4. 使用toMap()
方法处理重复键
流API提供了**toMap()
收集器方法,用于将流收集到一个Map
中**。
此外,toMap()
方法允许我们指定一个合并函数,用于处理与重复键关联的值。
例如,我们可以使用简单的lambda表达式,如果某个城市名称已经被收集过,则忽略后续的城市:
Map<String, City> resultMap1 = CITY_INPUT.stream()
.collect(toMap(City::getName, Function.identity(), (first, second) -> first));
assertEquals(4, resultMap1.size());
assertEquals(new City("Paris", "France"), resultMap1.get("Paris"));
如上测试所示,由于法国的Paris
出现在列表中的位置早于美国得克萨斯州的Paris
,最终的地图只包含位于法国的Paris
。
另一种选择是,当遇到重复键时,总是替换地图中的现有条目,我们可以调整lambda表达式以返回第二个City
对象:
Map<String, City> resultMap2 = CITY_INPUT.stream()
.collect(toMap(City::getName, Function.identity(), (first, second) -> second));
assertEquals(4, resultMap2.size());
assertEquals(new City("Paris", "Texas, USA"), resultMap2.get("Paris"));
如果运行测试,它会通过。所以这次,键"Paris"与美国得克萨斯州的Paris
相关联。
当然,在实际项目中,我们可能有比简单地跳过或覆盖更复杂的合并要求。我们总可以在合并函数中实现所需的合并逻辑。
最后,让我们看一个例子,将两个"Paris"城市的locatedIn
属性合并为一个新的City
实例,并将这个新的合并后的Paris
实例添加到地图结果中:
Map<String, City> resultMap3 = CITY_INPUT.stream()
.collect(toMap(City::getName, Function.identity(), (first, second) -> {
String locations = first.getLocatedIn() + " and " + second.getLocatedIn();
return new City(first.getName(), locations);
}));
assertEquals(4, resultMap2.size());
assertEquals(new City("Paris", "France and Texas, USA"), resultMap3.get("Paris"));
5. 总结
在这篇文章中,我们探讨了两种使用流API生成Map
结果时处理重复键的方法:
-
groupingBy()
- 创建Map
结果,类型为Map<Key, List<Value>>
-
mapTo()
- 允许我们在merge
函数中实现合并逻辑
如往常一样,这里展示的所有代码片段可在GitHub上找到。