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上找到。


» 下一篇: Maven Reactor