合并具有重复键的数组映射

18

我有两个数组映射。

Map<String, List<String>> map1 = new HashMap<>();
Map<String, List<String>> map2 = new HashMap<>();

我希望将它们合并到一个新的映射中。
如果一个键在两个映射中都存在,则应合并数组。

例如:

map1.put("k1", Arrays.asList("a0", "a1"));
map1.put("k2", Arrays.asList("b0", "b1"));

map2.put("k2", Arrays.asList("z1", "z2"));

// Expected output is 
Map 3: {k1=[a0, a1], k2=[b0, b1, z1, z2]}

我尝试使用流来完成这个操作

Map<String, List<String>> map3 = Stream.of(map1, map2)
    .flatMap(map -> map.entrySet().stream())
    .collect(Collectors.toMap(
        Map.Entry::getKey,
        e -> e.getValue().stream().collect(Collectors.toList())
    ));

如果在地图中没有相同的键,这将有效。否则,我会得到异常。

Exception in thread "main" java.lang.IllegalStateException: Duplicate key k2 (attempted merging values [b0, b1] and [z1, z2])
    at java.base/java.util.stream.Collectors.duplicateKeyException(Collectors.java:133)
    at java.base/java.util.stream.Collectors.lambda$uniqKeysMapAccumulator$1(Collectors.java:180)
    at java.base/java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
    at java.base/java.util.HashMap$EntrySpliterator.forEachRemaining(HashMap.java:1751)
    at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:658)
    at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:274)
    at java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
    at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
    at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
    at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:913)
    at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578)
    at im.djm.Test.main(Test.java:25)

有没有使用流来完成这个任务的方法?
还是我必须遍历映射?

2
不使用流也没有任何问题。相反,这样更易读且更快。 - Has QUIT--Anony-Mousse
8个回答

22

在存在重复键的情况下,请使用合并函数:

Map<String, List<String>> map3 = Stream.of(map1, map2)
                .flatMap(map -> map.entrySet().stream())
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        e -> new ArrayList<>(e.getValue()),
                        (left, right) -> {left.addAll(right); return left;}
                ));

注意,我已将 e -> e.getValue().stream().collect(Collectors.toList()) 改为 new ArrayList<>(e.getValue()),以确保我们始终拥有可变列表,可以在合并函数中添加。


4

您需要使用重载的toMap()版本,它允许合并重复键:

toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper,
                                    BinaryOperator<U> mergeFunction) 

您可以写成以下内容:

您可以写:

Map<String, List<String>> map3 = Stream.of(map1, map2)
    .flatMap(map -> map.entrySet().stream())
    .collect(Collectors.toMap(
        Map.Entry::getKey,
        e -> new ArrayList<>(e.getValue()),
        (e1, e2) -> { e1.addAll(e2); return e1;}
    ));

4
您还可以像这样做:
Map<String, List<String>> map3 = Stream.concat(map1.entrySet().stream(),
                                               map2.entrySet().stream())
      .collect(Collectors.groupingBy(Entry::getKey,
                   Collectors.mapping(Entry::getValue,
                       Collectors.flatMapping(List::stream,
                           Collectors.toList()))));

6
我认为,使用单个 flatMapping(e -> e.getValue().stream(), ...) 要比 Collectors.mapping(Entry::getValue, Collectors.flatMapping(List::stream, ...)) 更简单。它不一定总是需要方法引用。另外,虽然 OP 使用的是 Java 9,但问题已经被标记为 Java 8,所以值得注意的是,flatMapping 需要 Java 9。 - Holger

4

也许是这样。但是通过手动组合条目并使用迭代,你更有可能做到一切无误。我不知道是否还有其他人需要处理这段代码,但他们可能会感激这种易于阅读的方法。


3

使用两次 flatMap

Map<String, List<String>> map1 = new HashMap<>();
Map<String, List<String>> map2 = new HashMap<>();

map1.put("k1", Arrays.asList("a0", "a1"));
map1.put("k2", Arrays.asList("b0", "b1"));

map2.put("k2", Arrays.asList("z1", "z2"));

Map<String, List<String>> map3 = Stream.of(map1, map2)
        .flatMap(p -> p.entrySet().stream())
        .flatMap(p -> p.getValue().stream().map(q -> new Pair<>(p.getKey(), q)))
        .collect(
                Collectors.groupingBy(
                        p -> p.getKey(),
                        Collectors.mapping(p -> p.getValue(), Collectors.toList())
                )
        );

这个的工作方式如下:
  • 获取两个映射:Stream<Map<String,List<String>>>
  • 将条目作为Entry<String, List<String>> 扁平化
  • 将每个条目扁平化为一个Pair<String, String>
  • 按照它们的键进行收集
    • 获取值,并将其收集到列表中

请注意,HTML标签已被保留。

0

这里是一个使用两个映射的迭代示例。第一次迭代将来自map1和map2的公共键/值对连接在一起,并将它们添加到结果映射中,或者将map1中的唯一键/值对添加到结果映射中。第二次迭代获取map2中未匹配map1的任何剩余内容,并将其添加到结果映射中。

public static Map<String, ArrayList<String>> joinMaps(Map<String, ArrayList<String>> map1, Map<String, ArrayList<String>> map2)
{
    Map<String, ArrayList<String>> mapJoined = new HashMap<>();

    //join values from map2 into values of map1 or add unique key/values of map1
    for (Map.Entry<String, ArrayList<String>> entry : map1.entrySet()) {
        String key = entry.getKey();
        ArrayList<String> value = entry.getValue();
        if(map2.containsKey(key))
        {
            value.addAll(map2.get(key));
            mapJoined.put(key, value);
        }
        else
            mapJoined.put(key, value);
    }

    //add the non-duplicates left over in map 2
    for (Map.Entry<String, ArrayList<String>> entry : map2.entrySet()) {
        if(!mapJoined.containsKey(entry.getKey()))
            mapJoined.put(entry.getKey(), entry.getValue());
    }

    return mapJoined;
}

你也可以在函数中添加一个Set来跟踪第一次迭代中添加的所有键,然后如果该Set的大小==map2的大小,则知道这两个映射具有相同的键,无需迭代第二个映射,即map2。

0

另一种方法是这样的。

您应该使用较大的地图(此处为map1)初始化map3。然后循环遍历其他地图并使用merge方法组合重复键。

Map<String, List<String>> map3 = new HashMap<>(map1);
    for (Map.Entry<String, List<String>> entry : map2.entrySet()) {
       List<String> values = new ArrayList<>(entry.getValue());
       map3.merge(entry.getKey(),entry.getValue(),(l1, l2) -> {values.addAll(l1); 
           return values;
       });
    }

map2.forEach((key, value) -> {
    List<String> values = new ArrayList<>(value);
      map3.merge(key,value, (l1, l2) -> {values.addAll(l1);return values;});
});

0

这里有另一种合并地图和列表的方法。

Map<String, List<String>> map3 = Stream.of(map1, map2)
    .flatMap(map -> map.entrySet().stream())
    .collect(Collectors.toMap(
        Map.Entry::getKey,
        Map.Entry::getValue,
        (e1, e2) -> Stream.concat(e1.stream(), e2.stream()).collect(Collectors.toList())
    ));

toMap方法中的第三个参数是
(e1, e2) -> Stream.concat(e1.stream(), e2.stream()).collect(Collectors.toList()),它是mergeFunction函数。
此函数应用于重复项。

如果映射键包含重复项(根据Object.equals(Object))),则将值映射函数应用于每个相等元素,并使用提供的合并函数合并结果。
JavaDoc


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