使用流生成地图时忽略重复项

394
Map<String, String> phoneBook = people.stream()
                                      .collect(toMap(Person::getName,
                                                     Person::getAddress));

当发现重复元素时,我会收到java.lang.IllegalStateException: Duplicate key错误提示。

在向地图中添加值时,是否有可能忽略这样的异常?

当存在重复项时,它应该通过忽略该重复键来继续执行。


如果您使用HashSet,它将忽略已经存在的键。 - sahitya
@captain-aryabhatta。在HashSet中是否可以有键值对? - Patan
13个回答

655

使用Collectors.toMap(keyMapper, valueMapper, mergeFunction)方法的mergeFunction参数即可实现:

Map<String, String> phoneBook = 
    people.stream()
          .collect(Collectors.toMap(
             Person::getName,
             Person::getAddress,
             (address1, address2) -> {
                 System.out.println("duplicate key found!");
                 return address1;
             }
          ));

mergeFunction是一个操作与同一键相关联的两个值 的函数。adress1对应于在收集元素时遇到的第一个地址,adress2对应于遇到的第二个地址:这个lambda函数仅告诉我们保留第一个地址并忽略第二个地址。


14
为什么不允许出现重复的 (不是键)让我感到困惑,如何允许重复的值? - Hendy Irawan
2
如果发生冲突,是否可以完全忽略这个条目?基本上,如果我遇到重复的键,我不想添加它们。在上面的例子中,我不希望将address1或address2添加到我的映射中。 - djkelly99
31
@Hendy Irawan:允许存在重复值。合并函数是选择(或合并)具有相同键的两个值之间的操作。 - Ricola
6
@djkelly99,实际上你可以这样做,只需让你的重新映射函数返回null即可。请参见toMap文档,其中指向了merge文档,描述道“如果重映射函数返回null,则删除该映射。” - Ricola
4
我们应该将address2返回以模仿标准映射行为。如果这是一个for each而不是collect,那么标准行为将是在第二个地址上放置会清除第一个地址。因此,为了避免代码重构时行为发生变化,选择返回address2是合乎逻辑的选择。 - lvoelk
显示剩余6条评论

183
根据 JavaDocs,如果映射的键包含重复项(根据Object.equals(Object)判断),则在执行集合操作时会抛出IllegalStateException。如果映射的键可能有重复项,则应改用toMap(Function keyMapper, Function valueMapper, BinaryOperator mergeFunction)。因此,您应该使用toMap(Function keyMapper, Function valueMapper, BinaryOperator mergeFunction),并提供一个合并函数,以确定将哪个重复项放入映射中。例如,如果您不在意哪一个,请调用:
Map<String, String> phoneBook = people.stream().collect(
        Collectors.toMap(Person::getName, Person::getAddress, (a1, a2) -> a1));

2
如果没有正确理解,这可能会导致严重的数据丢失。 - Anurag Bhalekar
1
是的,在大多数情况下,重复的值必须以某种方式合并或抛出异常(默认情况下这是正确的行为)。但在一些罕见的情况下,您需要忽略重复项,这就是问题所在。 - alaster

20

来自alaster的答案帮了我很多,但如果有人试图对数据进行分组,我想添加一些有意义的信息。

例如,如果你有两个具有相同code但不同quantity产品的Orders,并且你想求和它们的数量,你可以这样做:

List<Order> listQuantidade = new ArrayList<>();
listOrders.add(new Order("COD_1", 1L));
listOrders.add(new Order("COD_1", 5L));
listOrders.add(new Order("COD_1", 3L));
listOrders.add(new Order("COD_2", 3L));
listOrders.add(new Order("COD_3", 4L));

listOrders.collect(Collectors.toMap(Order::getCode, 
                                    o -> o.getQuantity(), 
                                    (o1, o2) -> o1 + o2));

结果:

{COD_3=4, COD_2=3, COD_1=9}

或者,根据Java文档,您可以合并地址:

 Map<String, String> phoneBook
     people.stream().collect(toMap(Person::getName,
                                   Person::getAddress,
                                   (s, a) -> s + ", " + a));

4

如果其他人遇到了这个问题,但在流中没有重复的键,请确保您的keyMapper函数不返回null值

这很恼人,因为当处理第二个元素时,异常会说“重复的键1”,而实际上1是该项的,而不是键。

在我的情况下,我的keyMapper函数试图在另一个map中查找值,但由于字符串中的拼写错误导致返回了null值。

final Map<String, String> doop = new HashMap<>();
doop.put("a", "1");
doop.put("b", "2");

final Map<String, String> lookup = new HashMap<>();
doop.put("c", "e");
doop.put("d", "f");

doop.entrySet().stream().collect(Collectors.toMap(e -> lookup.get(e.getKey()), e -> e.getValue()));

3

按对象分组

Map<Integer, Data> dataMap = dataList.stream().collect(Collectors.toMap(Data::getId, data-> data, (data1, data2)-> {LOG.info("Duplicate Group For :" + data2.getId());return data1;}));

如果值是字符串,你如何在这里记录键名? - bschandramohan

1

感觉toMap有时候工作,有时候不工作,这是Java Streams的一种黑暗面。就像他们应该把它叫做toUniqueMap或其他什么东西...

最简单的方法是使用Collectors.groupingBy而不是Collectors.toMap

默认情况下,它将返回List类型的输出,但冲突问题已经消失了,在存在多个元素的情况下,这可能正是您想要的。

  Map<String, List<Person>> phoneBook = people.stream()
          .collect(groupingBy((x) -> x.name));

如果需要将与特定名称关联的地址集合转换为Set类型的集合,groupingBy也可以实现此功能:
Map<String, Set<String>> phoneBook = people.stream()
          .collect(groupingBy((x) -> x.name, mapping((x) -> x.address, toSet())));

另一种方法是使用哈希表或集合来“开始”...并仔细跟踪以确保输出流中的键不重复。嗯。这里有一个示例,有时能够幸存...

1

我遇到了同样的问题。Map 存储键值对,不允许重复的键。如果单个对象具有重复的名称,则会出现错误

java.lang.IllegalStateException: 重复的键

例如:

Map<String, String> stringMap;
        List<Person> personList = new ArrayList<>();
        personList.add(new Person(1, "Mark", "Menlo Park"));
        personList.add(new Person(2, "Sundar", "1600 Amphitheatre Pkwy"));
        personList.add(new Person(3, "Sundar", "232 Santa Margarita Ave"));
        personList.add(new Person(4, "Steve", "Los Altos"));

        stringMap = personList.stream().distinct().collect(Collectors.toMap(Person::getName, Person::getAddress));

enter image description here

为了解决这个问题,我们需要使用一个带有额外参数的不同方法,即mergeFunction

    phoneBook = personList.stream().distinct().collect(Collectors.toMap(Person::getName, Person::getAddress
                    , (existing, replacement) -> existing));

System.out.println("Map object output :" + stringMap);

输出:Map对象输出:{Steve=洛斯阿尔托斯,Mark=门洛帕克,Sundar=1600 Amphitheatre Pkwy} 注意:当您更改 (existing, replacement) -> replacement) 时,旧键将被新值替换。 如果您需要所有地址存储相同的键,请查看此链接 Multimap

0

可以使用Lambda函数:比较是在从key(...)获取的键字符串上进行的

List<Blog> blogsNoDuplicates = blogs.stream()
            .collect(toMap(b->key(b), b->b, (b1, b2) -> b1))  // b.getAuthor() <~>key(b) as Key criteria for Duplicate elimination
            .values().stream().collect(Collectors.toList());

static String key(Blog b){
    return b.getTitle()+b.getAuthor(); // make key as criteria of distinction
}

0
为了完整起见,这里是如何将重复项“减少”到只剩一个的方法。
如果您对最后一个OK,那就可以。
  Map<String, Person> phoneBook = people.stream()
          .collect(groupingBy(x -> x.name, reducing(null, identity(), (first, last) -> last)));

如果你只想要第一个:

  Map<String, Person> phoneBook = people.stream()
          .collect(groupingBy(x -> x.name, reducing(null, identity(), (first, last) -> first != null ? first : last)));

如果你想要最后一个但是“地址为字符串”(不使用identity()作为参数)。

  Map<String, String> phoneBook = people.stream()
          .collect(groupingBy(x -> x.name, reducing(null, x -> x.address, (first, last) -> last)));

来源

因此,本质上,groupingByreducing收集器配对使用时,开始表现得非常类似于toMap收集器,具有类似于其mergeFunction的东西...并且具有相同的最终结果...


0
我在分组对象时遇到了这样的问题,我总是通过一种简单的方法来解决它们:使用java.util.Set执行自定义过滤器,以删除具有您选择的任何属性的重复对象,如下所示。
Set<String> uniqueNames = new HashSet<>();
Map<String, String> phoneBook = people
                  .stream()
                  .filter(person -> person != null && !uniqueNames.add(person.getName()))
                  .collect(toMap(Person::getName, Person::getAddress));

希望这可以帮助到有同样问题的任何人!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接