How to merge Stream<Map<String, Map<String, String>>> into a single Map<String, Map<String, String>>?

Viewed 4067

I am trying to merge a Stream<Map<String, Map<String, String>>> object into a single map with keys in all the Streams.

For example,

final Map<String, someOtherObjectToProcess> someObject;

final List<Map<String, Map<String, String>>> list = someObject.entrySet()
            .stream()
            .flatMap(this::getInfoStream)
            .collect(Collectors.toList());

The signature for getInfoStream is

public Stream<Map<String, Map<String, String>>> getInfoStream(Map.Entry<String, someOtherObjectToProcess> entry)

if I use (Collectors.toList()) I am able to get a list of these Map objects.

Sample output if I use the above code:

[{
    "name" : {
        "property":"value"
    }
},

{
    "name2" : {
        "property":"value"
    }
}]

But I want to collect into a Map with the structure

{
    "name" : {
        "property":"value"
    },
    "name2" : {
        "property":"value"
    }
}

Provided that the keys will be unique.

How can I do this with Collectors.toMap() or any other alternative way?

4 Answers

When you have

Stream<Map<String, Map<String, String>>> stream = ...

(which I am assuming is result of .flatMap(this::getInfoStream)) you can call

.flatMap(map -> map.entrySet().stream())

to create stream of entries from all maps which will produce Stream<Map.Entry<String, Map<String, String>>>.

Now from that stream all you need to do is collect key and value from each entry into map. Assuming each key will be unique across all maps you could use

.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

but if keys are not unique you need to decide what value should be placed in new map for same key. We can do it by filling ... part in

.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (vOld, vNew) -> ...));
//                                                                                ^^^

where vOld holds value currently held in result map under same key, and vNew holds new value (from current stream "iteration").
For instance if you want to ignore new value you can simply return old/currently held by (vOld, vNew) -> vOld

So in short (assuming unique keys):

Map<String, Map<String, String>> combinedMap = 
        /*your Stream<Map<String, Map<String, String>>>*/
        .flatMap(map -> map.entrySet().stream())
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

Another way to solve this would be not using a collector(toList()) but the other overloaded .collect() method with Supplier, Accumulator, and Combiner:

Stream<Map<String, Map<String, String>>> stream = ...
Map<String, Map<String, String>> result = stream
       .collect(HashMap::new, HashMap::putAll, HashMap::putAll);

The most readable way in my opinion is to map everything to a Map.Entry and then collect everything back to a Map using Collectors::toMap

import static java.util.stream.Collectors.toMap;

// ...

someObject.entrySet()
          .stream()
          .flatMap(this::getInfoStream)
          .flatMap(map -> map.entrySet().stream())
          .collect(toMap(Map.Entry::getKey, Map.Entry::getValue, (one, two) -> one));

(one, two) -> one is the merge function, basically if you have duplicates, you just arbitrarely take the first one to come up

TL;DR:

var merged = Stream.of(map1, map2, ..., mapN).reduce(new HashMap<>(), (a, b) -> {
    a.putAll(b);
    return a;
});

You can use reduce to combine a stream of Map<String, Map<String, String>> elements into one:

import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;

public class Main {

    public static void main(String[] args) {
        alternative1();
        alternative2();
    }

    // Use reduce without an initial identity value
    public static void alternative1() {
        Map<String, Map<String, String>> m1 = new HashMap<>();
        m1.put("name", Map.of("property", "value"));

        Map<String, Map<String, String>> m2 = new HashMap<>();
        m2.put("name2", Map.of("property", "value"));

        Stream<Map<String, Map<String, String>>> mapStream = Stream.of(m1, m2);

        Map<String, Map<String, String>> m3 = mapStream.reduce((a, b) -> {
            Map<String, Map<String, String>> temp = new HashMap<>();
            temp.putAll(a);
            temp.putAll(b);
            return temp;
        }).orElseThrow();

        System.out.println(m3);
    }

    // Use reduce with an initial empty map as the identity value
    public static void alternative2() {
        Map<String, Map<String, String>> m1 = new HashMap<>();
        m1.put("name", Map.of("property", "value"));

        Map<String, Map<String, String>> m2 = new HashMap<>();
        m2.put("name2", Map.of("property", "value"));

        Stream<Map<String, Map<String, String>>> mapStream = Stream.of(m1, m2);

        Map<String, Map<String, String>> m3 = mapStream.reduce(new HashMap<>(), (a, b) -> {
            a.putAll(b);
            return a;
        });

        System.out.println(m3);
    }
}

Output:

{name={property=value}, name2={property=value}}
{name={property=value}, name2={property=value}}

But beware that these solutions assume keys (name and name2) are unique, otherwise duplicate keys would make map entries overwrite each other.


The same logic with a more modern syntax:

import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;

public class Main {

    public static void main(String[] args) {
        alternative1();
        alternative2();
    }

    // Use reduce without an initial identity value
    public static void alternative1() {
        var m1 = Map.of("name", Map.of("property", "value"));
        var m2 = Map.of("name2", Map.of("property", "value"));
        var m3 = Stream.of(m1, m2).reduce((a, b) -> {
            var temp = new HashMap<String, Map<String, String>>();
            temp.putAll(a);
            temp.putAll(b);
            return temp;
        }).orElseThrow();

        System.out.println(m3);
    }

    // Use reduce with an initial empty map as the identity value
    public static void alternative2() {
        var m1 = Map.of("name", Map.of("property", "value"));
        var m2 = Map.of("name2", Map.of("property", "value"));
        var m3 = Stream.of(m1, m2).reduce(new HashMap<>(), (a, b) -> {
            a.putAll(b);
            return a;
        });

        System.out.println(m3);
    }
}
Related