djm.im
djm.im

Reputation: 3323

Merge map of arrays with duplicate keys

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?

Upvotes: 18

Views: 24060

Answers (8)

djm.im
djm.im

Reputation: 3323

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

Upvotes: 1

Hadi
Hadi

Reputation: 17289

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

Upvotes: 0

RAZ_Muh_Taz
RAZ_Muh_Taz

Reputation: 4089

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.

Upvotes: 0

Andreas
Andreas

Reputation: 159086

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

Upvotes: 4

jrtapsell
jrtapsell

Reputation: 6991

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

Upvotes: 3

davidxxx
davidxxx

Reputation: 131326

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

Upvotes: 4

Ousmane D.
Ousmane D.

Reputation: 56393

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.

Upvotes: 24

Steve11235
Steve11235

Reputation: 2923

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.

Upvotes: 4

Related Questions