novice
novice

Reputation: 820

how to merge a collection of Maps using streams

I have a collection of maps:

Collection<Map<String,Double>> myCol = table.values();

I would like to transform this into a Map

Map<String, Double>

such that, for a matching key, values are summed up. Using a for loop, it is rather simple:

Map<String, Double> outMap = new HashMap<>();
for (Map<String, Double> map : myCol) {
    outMap =  mergeMaps(outMap, map);
}

and mergeMaps() is defined as

mergeMaps(Map<String, Double> m1, Map<String, Double> m2){    
    Map<String, Double> outMap = new TreeMap<>(m1);
    m2.forEach((k,v) -> outMap.merge(k,v,Double::sum)); /*sum values if key exists*/
    return outMap;
}

However, I would like to use streams to get a map from collection. I have tried as follows:

Map<String, Double> outMap = new HashMap<>();    
myCol.stream().forEach(e-> outMap.putAll(mergeMaps(outMap,e)));
return outMap;

This works without a problem. However, can I still improve it? I mean, how can I use collectors in it?

Upvotes: 14

Views: 15113

Answers (4)

fps
fps

Reputation: 34470

With streams:

Map<String, Double> outMap = myCol.stream()
    .flatMap(map -> map.entrySet().stream())
    .collect(Collectors.toMap(
        Map.Entry::getKey,   // key of the result map
        Map.Entry::getValue, // value of the result map
        Double::sum,         // how to merge values for equal keys
        TreeMap::new));      // the type of map to be created

This uses Collectors.toMap to create the result TreeMap.

You can do it without streams, though. I think your version is a little bit complicated, you could refactor it as follows:

Map<String, Double> outMap = TreeMap<>();
myCol.forEach(map -> map.forEach((k, v) -> outMap.merge(k, v, Double::sum)));

Which is shorter, easy and most readable.

Upvotes: 1

Malte Hartwig
Malte Hartwig

Reputation: 4553

Well, the other proposed solutions show that a pure stream solution is short, but if you wanted to use your existing mergeFunction (because in other cases it is more complex for example), you could just hand it over to Stream.reduce:

Optional<Map<String, Double>> outMap = myCol.stream().reduce((m1, m2) -> mergeMaps(m1, m2));

Your initial approach with the forEach is pretty much a streamyfied for loop and violates the concept of functions having no side effects. The reduce (or the above collects) handles all the data merging internally, without changing the input collection.

Upvotes: 1

Alexis C.
Alexis C.

Reputation: 93892

From your input, you can fetch the stream of maps and then flatmap it to have a Stream<Map.Entry<String, Double>>. From there, you collect them into a new map, specifying that you want to sum the values mapped to the same key.

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

....

Map<String, Double> outMap = 
    myCol.stream()
         .flatMap(m -> m.entrySet().stream())
         .collect(toMap(Map.Entry::getKey, Map.Entry::getValue, Double::sum));

Alternatively, you can use groupingBy instead of toMap:

.collect(groupingBy(Map.Entry::getKey, summingDouble(Map.Entry::getValue)));

Upvotes: 12

Eugene
Eugene

Reputation: 121068

 myCol.stream()
        .flatMap(x -> x.entrySet().stream())
        .collect(Collectors.groupingBy(
                Entry::getKey, 
                TreeMap::new, 
                Collectors.summingDouble(Entry::getValue)));

Upvotes: 2

Related Questions