Merge map of arrays with duplicate keys

夙愿已清 提交于 2021-02-04 13:12:27

问题


I have two maps of arrays.

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

I want to merge them in one new map.
If a key exists in both maps, in that case, I should merge arrays.

For example:

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]}

I tried to do that with streams

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())
    ));

This work if there are no the same keys in maps. Otherwise, I get the exception

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)

Is there a way to accomplish this task with streams?
Or I have to iterate throug maps?


回答1:


Use a merge function in the case of duplicate keys:

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;}
                ));

Note, I've changed e -> e.getValue().stream().collect(Collectors.toList()) to new ArrayList<>(e.getValue()) to guarantee that we always have a mutable list which we can add into in the merge function.




回答2:


Maybe. But you are more likely to get everything right by combining the entries manually, using iteration. I don't know if anyone else will have to work on this code, but they will likely be grateful for an easy to read approach.




回答3:


You can also do it like this:

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()))));



回答4:


You have to use the overloaded toMap() version that allows to merge duplicate keys :

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

You could write something as :

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;}
    ));



回答5:


Using flatmap twice

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())
                )
        );

This works like this:

  • Takes both maps Stream<Map<String,List<String>>>
  • FlatMaps the entries as Entry<String, List<String>>
  • FlatMaps the entries into 1 pair per Pair<String, String>
  • Collects them by their key
    • Taking the values, and collecting them into a list



回答6:


Here is an example using iteration of both maps. First iteration joins common key/value pairs from map1 and map2 together and adds them to the resulting map or adds unique key/value pairs in map1 to the resulting map. Second iteration grabs anything leftover in map2 that didnt match map1 and adds them to the resulting map.

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;
}

You could also add a Set into the function to keep track of all keys added in the first iteration, then if the size of that Set == size of the map2 you know the maps have the same keys and there is no need to iterate second map, map2.




回答7:


other way would be like this.

you should init map3 with larger map.(here map1). then use loop over other map and use merge method to combine duplicate key.

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;});
});



回答8:


Here is one another way to merge maps and lists.

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())
    ));

The third argument in toMap method is
(e1, e2) -> Stream.concat(e1.stream(), e2.stream()).collect(Collectors.toList()) is mergeFunction function.
This function is applied to duplicates.

If the mapped keys contains duplicates (according to Object.equals(Object)), the value mapping function is applied to each equal element, and the results are merged using the provided merging function.
JavaDoc



来源:https://stackoverflow.com/questions/50379529/merge-map-of-arrays-with-duplicate-keys

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!