3

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?

2
  • 1
    show the signature of getInfoStream and what someObject looks like, please Commented Jun 8, 2019 at 11:34
  • 1
    for some reason, it seems like the cause of the problem would be the implementation of getInfoStream which is returning a Stream<Map<>> and not even Stream<Map.Entry>s. Commented Jun 8, 2019 at 11:45

4 Answers 4

5

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));
Sign up to request clarification or add additional context in comments.

1 Comment

In my case, the maps keys are unique and updated the question.
1

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

Comments

1

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

Comments

1

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.