Reputation: 820
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
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
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
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
Reputation: 121068
myCol.stream()
.flatMap(x -> x.entrySet().stream())
.collect(Collectors.groupingBy(
Entry::getKey,
TreeMap::new,
Collectors.summingDouble(Entry::getValue)));
Upvotes: 2