Merging two Map<String, Integer> with Java 8 Stream API

JavaMergeJava 8Java Stream

Java Problem Overview


I have two (or more) Map<String, Integer> objects. I'd like to merge them with Java 8 Stream API in a way that values for common keys should be the maximum of the values.

@Test
public void test14() throws Exception {
    Map<String, Integer> m1 = ImmutableMap.of("a", 2, "b", 3);
    Map<String, Integer> m2 = ImmutableMap.of("a", 3, "c", 4);
    List<Map<String, Integer>> list = newArrayList(m1, m2);

    Map<String, Integer> mx = list.stream()... // TODO
    
    Map<String, Integer> expected = ImmutableMap.of("a", 3, "b", 3, "c", 4);
    assertEquals(expected, mx);
}

How can I make this test method green?

I've played with collect and Collectors for a while without any success.

(ImmutableMap and newArrayList are from Google Guava.)

Java Solutions


Solution 1 - Java

@Test
public void test14() throws Exception {
    Map<String, Integer> m1 = ImmutableMap.of("a", 2, "b", 3);
    Map<String, Integer> m2 = ImmutableMap.of("a", 3, "c", 4);

    Map<String, Integer> mx = Stream.of(m1, m2)
        .map(Map::entrySet)          // converts each map into an entry set
        .flatMap(Collection::stream) // converts each set into an entry stream, then
                                     // "concatenates" it in place of the original set
        .collect(
            Collectors.toMap(        // collects into a map
                Map.Entry::getKey,   // where each entry is based
                Map.Entry::getValue, // on the entries in the stream
                Integer::max         // such that if a value already exist for
                                     // a given key, the max of the old
                                     // and new value is taken
            )
        )
    ;

    /* Use the following if you want to create the map with parallel streams
    Map<String, Integer> mx = Stream.of(m1, m2)
        .parallel()
        .map(Map::entrySet)          // converts each map into an entry set
        .flatMap(Collection::stream) // converts each set into an entry stream, then
                                     // "concatenates" it in place of the original set
        .collect(
            Collectors.toConcurrentMap(        // collects into a map
                Map.Entry::getKey,   // where each entry is based
                Map.Entry::getValue, // on the entries in the stream
                Integer::max         // such that if a value already exist for
                                     // a given key, the max of the old
                                     // and new value is taken
            )
        )
    ;
    */

    Map<String, Integer> expected = ImmutableMap.of("a", 3, "b", 3, "c", 4);
    assertEquals(expected, mx);
}

Solution 2 - Java

Map<String, Integer> mx = new HashMap<>(m1);
m2.forEach((k, v) -> mx.merge(k, v, Integer::max));

Solution 3 - Java

mx = list.stream().collect(HashMap::new,
		(a, b) -> b.forEach((k, v) -> a.merge(k, v, Integer::max)),
		Map::putAll);

This covers the general case for any size list and should work with any types, just swap out the Integer::max and/or HashMap::new as desired.

If you don't care which value comes out in a merge, there's a much cleaner solution:

mx = list.stream().collect(HashMap::new, Map::putAll, Map::putAll);

And as generic methods:

public static <K, V> Map<K, V> mergeMaps(Stream<? extends Map<K, V>> stream) {
	return stream.collect(HashMap::new, Map::putAll, Map::putAll);
}

public static <K, V, M extends Map<K, V>> M mergeMaps(Stream<? extends Map<K, V>> stream,
		BinaryOperator<V> mergeFunction, Supplier<M> mapSupplier) {
	return stream.collect(mapSupplier,
			(a, b) -> b.forEach((k, v) -> a.merge(k, v, mergeFunction)),
			Map::putAll);
}

Solution 4 - Java

I've created a visual representation of what @srborlongan did, for anyone who might be interested.

Diagram displaying maps convert to stream of entries

Solution 5 - Java

I added my contribution to the proton pack library which contains utility methods for the Stream API. Here's how you could achieve what you want:

Map<String, Integer> mx = MapStream.ofMaps(m1, m2).mergeKeys(Integer::max).collect();

Basically mergeKeys will collect the key-value pairs in a new map (providing a merge function is optional, you'll end up with a Map<String, List<Integer>> otherwise) and recall stream() on the entrySet() to get a new MapStream. Then use collect() to get the resulting map.

Solution 6 - Java

Using StreamEx you can do:

StreamEx.of(m1, m2)
    .flatMapToEntry(x -> x)
    .grouping(IntCollector.max())

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionuser3528157View Question on Stackoverflow
Solution 1 - JavasrborlonganView Answer on Stackoverflow
Solution 2 - JavaStuart MarksView Answer on Stackoverflow
Solution 3 - JavaSean Van GorderView Answer on Stackoverflow
Solution 4 - Javagiannis christofakisView Answer on Stackoverflow
Solution 5 - JavaAlexis C.View Answer on Stackoverflow
Solution 6 - JavaGraeme MossView Answer on Stackoverflow