29

I have two maps whose keys are Strings and whose values are Set<MyObject>. Given two Maps, what is the easiest way to merge them such that if two keys are identical, the value is a union of the two sets. You can assume values are never null and if it is useful, we can make these Maps SortedMaps.

Bhesh Gurung
  • 50,430
  • 22
  • 93
  • 142
Dave
  • 15,639
  • 133
  • 442
  • 830
  • 3
    If you have the possiblity to use Guavas [Multimap](https://code.google.com/p/guava-libraries/wiki/NewCollectionTypesExplained#Multimap), you can simply avoid the problem, and merging is as simple as putAll(Multimap other). – Dag May 29 '15 at 15:22
  • similar, possibly duplicate: http://stackoverflow.com/questions/4299728/how-can-i-combine-two-hashmap-objects-containing-the-same-types – Alec Gilliland Aug 20 '15 at 18:20
  • Should be easy to do with the [Map merge](https://docs.oracle.com/javase/8/docs/api/java/util/Map.html#merge-K-V-java.util.function.BiFunction-) method. – Roland Aug 29 '16 at 16:29

11 Answers11

31

You can do this with a stream fairly easily:

Map<T, Set<U>> merged = Stream.of(first, second)
        .map(Map::entrySet)
        .flatMap(Set::stream)
        .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (a, b) -> {
            HashSet<U> both = new HashSet<>(a);
            both.addAll(b);
            return both;
        }));

This splits the maps into their Entrys and then joins them with a Collector which resolves duplicates by adding both values to a new HashSet.

This also works for any number of maps.

Some variations which produce the same result:

Stream.of(first, second).flatMap(m -> m.entrySet().stream())
    .collect(...);
Stream.concat(first.entrySet().stream(), second.entrySet().stream())
    .collect(...); //from comment by Aleksandr Dubinsky

The third parameter for Collectors.toMap is not necessary if there are no duplicate keys.

There is another Collectors.toMap with a fourth parameter that lets you decide the type of the Map collected into.

Alex - GlassEditor.com
  • 14,957
  • 5
  • 49
  • 49
14

Are we talking about HashMap instances. In that case lookup is O(1), so you can just take one map, iterate over the entries of that map, see whether the other map contains that key. If not, just add the set. If it contains the key, take the union of the two sets (by adding all elements of one set to another)

To illustrate with some code, where I used a Set to have autocompletion in my IDE

Map<String, Set<Double>> firstMap = new HashMap<String, Set<Double>>(  );
Map<String, Set<Double>> secondMap = new HashMap<String, Set<Double>>(  );
Set<Map.Entry<String, Set<Double>>> entries = firstMap.entrySet();
for ( Map.Entry<String, Set<Double>> entry : entries ) {
  Set<Double> secondMapValue = secondMap.get( entry.getKey() );
  if ( secondMapValue == null ) {
    secondMap.put( entry.getKey(), entry.getValue() );
  }
  else {
    secondMapValue.addAll( entry.getValue() );
  }
}
Robin
  • 36,233
  • 5
  • 47
  • 99
  • 4
    This will skip entries that exist in secondMap but not in firstMap – sam May 13 '13 at 06:21
  • sam - he is editing the secondMap in place, so secondMap will be changed. – JFK Feb 23 '15 at 15:15
  • you can use - addAll method http://download.oracle.com/javase/6/docs/api/java/util/HashMap.html But there is always this issue that - if your two hash maps have any key same - then it will override the value of the key from first hash map with the value of the key from second hash map. For being on safer side - change the key values - you can use prefix or suffix on the keys - ( different prefix/suffix for first hash map and different prefix/suffix for second hash map ) – Ashish Shetkar Mar 16 '16 at 06:25
6
static void mergeSet(Map<String, Set<String>> map1, Map<String, Set<String>> map2) {
    map1.forEach((key1, value1) -> {
        map2.merge(key1, value1, (key2, value2) -> key2).addAll(value1);
    });
}
Suban Asif
  • 71
  • 1
  • 3
  • That is not totally right.. merge function signature is more like `merge(T key, V value, Bifunction` . so the right answer should be something like `map1.forEach((key1, value1) -> { map2.merge(key1, value1, (valu1, value2) -> doSomethingWithValues); }); – kszosze Sep 22 '20 at 09:03
4

How about this (untested):

Map<String,Set<Whatever>> m1 = // input map
Map<String,Set<Whatever>> m2 =  // input map

Map<String,Set<Whatever>> ret =  // new empty map
ret.putAll(m1);

for(String key : m2.keySet()) {
    if(ret.containsKey(key)) {
        ret.get(key).addAll(m2.get(key));
    } else {
        ret.put(key,m2.get(key));
    }
}

This solution doesn't modify the input maps, and because it is short and relies on API methods only, I find it quite readable.

Note that putAll() and addAll() are both optional methods in Map and Set. Consequently (and in order to get O(1) lookup), I'd recommend using HashMap and HashSet.

Note that because neither HashSet or HashMap are synchronised you will need to look for some other solution if you want thread-safe code.

Timothy Jones
  • 21,495
  • 6
  • 60
  • 90
1

The following should merge a map1 into map2 (untested):

for (Entry<String, Set<???>> entry : map1.entrySet( ))
{
    Set<???> otherSet = map2.get(entry.getKey( ));
    if (otherSet == null)
        map2.put(entry.getKey( ), entry.getValue ( ));
    else
        otherSet.addAll(entry.getValue( ));
}

I don't know what you've parameterized your Sets on, hence the <???>: replace as appropriate.

Mac
  • 14,615
  • 9
  • 62
  • 80
1

Something like this (untested):

// Assume all maps are of the same generic type.
public static Map<String, Set<MyObject>> mergeAll(Map m1, Map m2) {
  Map<String, Set<MyObject>> merged = new HashMap();
  // Merge commom entries into the new map.
  for (Map.Entry<String, Set<MyObject>> entry : m1.entrySet()) {
    String key = entry.getKey();
    Set<MyObject> s1 = new HashSet(entry.getValue());
    Set<MyObject> s2 = m2.get(key);
    if (s2 != null) s1.addAll(s2);
    merged.put(key, s1);
  }
  // Add entries unique to m2 to the new map.
  for (String key : m2.keys()) {
    if (!s1.containsKey(key)) merged.put(key, new HashSet(m2.get(key)));
  }
  return merged;
}

Note that this solution does not mutate either of its arguments.

maerics
  • 151,642
  • 46
  • 269
  • 291
0
Map<Integer,String> m1=new HashMap<Integer,String>();
Map<Integer,String> m2=new HashMap<Integer,String>();
m1.put(1,"one");
m1.put(2,"two");
m2.put(3,"three");
m2.put(2,"two");
Set<Integer> s=m2.keySet();
for(int i:s){
    if(m1.get(i)==null){
        m1.put(i,m2.get(i));
    }
}
System.out.println(m1);
0

Note that all other answers will eventually augment the original sets which you might not want for all use cases, if you don't want that just use a third map as output and create a new set for each key

public static void merge2Maps(Map<String, Set<Double>> a, Map<String, Set<Double>> b, Map<String, Set<Double>> c){

    for (Map.Entry<String, Set<Double>> entry : a.entrySet()) {
        Set<Double> set = new HashSet<Double>();
        c.put(entry.getKey(), set);
        set.addAll(entry.getValue());
    }

    for (Map.Entry<String, Set<Double>> entry : b.entrySet()) {
        String key = entry.getKey();
        Set<Double> set = c.get(key);

        if (set == null) {
            set = new HashSet<Double>();
            c.put(entry.getKey(), set);
        }

        set.addAll(entry.getValue());
    }
}
Mmmh mmh
  • 5,334
  • 3
  • 21
  • 29
0

If you want to end up with immutable data structures to prevent manipulation of your merged map and map's Set instances then you can take this approach. This solution uses Google's Guava library.

public <K,T> Map<K, Set<T>> mergeToImmutable (
    final Map<K, Set<T>> left,
    final Map<K, Set<T>> right)
{
    return Maps.toMap(
        Sets.union(
            checkNotNull(left).keySet(),
            checkNotNull(right).keySet()
        ),
        new Function<K, Set<T>> () {
            @Override
            public Set<T> apply (K input) {
                return ImmutableSet.<T>builder()
                    .addAll(MoreObjects.firstNonNull(left.get(input), Collections.<T>emptySet()))
                    .addAll(MoreObjects.firstNonNull(right.get(input), Collections.<T>emptySet()))
                    .build();
            }
        }
    );
}
justin.hughey
  • 1,246
  • 15
  • 16
0

If you define a method to unite non-null Sets as:

static <T> Set<T> union(Set<T>... sets) {
    return Stream.of(sets)
                 .filter(s -> s != null)
                 .flatMap(Set::stream)
                 .collect(Collectors.toSet());
}

then merging two maps m1 and m2 having Set<V> values can be performed as follows:

Map<String, V> merged
    = union(m1.keySet(), m2.keySet())
           .stream()
           .collect(Collectors.toMap(k -> k, k -> union(m1.get(k), m2.get(k)))); 

Or even simpler:

Map<String, V> merged = new HashMap<>();
for (String k : union(m1.keySet(), m2.keySet())
     merged.put(k, union(m1.get(k), m2.get(k)));
Alex Salauyou
  • 14,185
  • 5
  • 45
  • 67
0
<K, V> Map<K, List<V>> mergeMapOfLists(Stream<Map<K, List<V>>> stream) {
    return stream
            .map(Map::entrySet) // convert each map to set of map's entries
            .flatMap(Collection::stream) // convert each map entry to stream and flat them to one stream
            .collect(toMap(Map.Entry::getKey, Map.Entry::getValue,
                    (list1, list2) -> {
                        list1.addAll(list2);
                        return list1;
                    })); // convert stream to map; if key is duplicated execute merge fuction (append exisitng list with elements from new list)
}
Karol Król
  • 3,320
  • 1
  • 34
  • 37