2

I have n maps of the kind:

HashMap<String,Double> map1;
HashMap<String,Double> map2; ...

What could I do to merge all these maps into a single map?

And I want to achieve this without losing the data.

Example:

map1 has entries: {<X1, Y1>, <X2,Y2>, <X3,Y3>} 
map2 has entries: {<X1, Y6>, <X2, Y7>, <X3,Y8>}
mergedMap: {<X1, Y1+Y6>, <X2,Y2+Y7>, <X3, Y3+Y8>}

I have tried this:

ArrayList<HashMap<String, Double>> listHashMap = new ArrayList<>();
            
HashMap<String,Double> mergedMap = new HashMap<>();
for(HashMap<String,Double> map: listHashMap) {  
    mergedMap.putAll(map);          
}

But I've understood that the values mapped to the same key would be replaced (put), not added.

What could I do to add up each value mapped to the same key?

Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
redbite
  • 189
  • 1
  • 2
  • 10
  • Would `HashMap > mergedMap` work for you? – Old Dog Programmer May 12 '22 at 18:40
  • @James It should be `Map>`, not HashMap and ArrayList. – David Conrad May 12 '22 at 18:41
  • If you were using Guava MultiMaps, this Q&A might help: https://stackoverflow.com/questions/51526988/merge-two-guava-multimaps – David Conrad May 12 '22 at 18:49
  • @DavidConrad i'm not using Multimaps but i've read about it and i found it interesting. Will look into it, thanks! – redbite May 12 '22 at 19:05
  • There was some room for different interpretations in the original edition of the question. My interpretation was the O/P wanted to associate multiple values with one key in the merged map. In the example, mergedMap: {, , }, I read "Y1+Y6" as "Y1 and Y6", similar to using + to indicate concatenation. Only after seeing the edit by Alexander Ivanchenko did I see what addition was what was wanted. – Old Dog Programmer May 15 '22 at 14:10
  • 1
    @James As i was using Double values, i thought this was assumable. But yes, the edit from Alexander was definitely clearer! thanks for your response too. – redbite May 15 '22 at 21:01

6 Answers6

4

You can utilize Java 8 method merge() to combine the data of each entry:

List<Map<String, Double>> list = // initializing source list
Map<String, Double> mergedMap = new HashMap<>();

list.forEach(map -> map.forEach((k, v) -> mergedMap.merge(k, v, Double::sum)));

See that code run live at Ideone.com.

Or you can make use of Stream API with combination of built-in collectors groupingBy() and summingDouble():

Map<String, Double> mergedMap = list.stream()
            .flatMap(map -> map.entrySet().stream())
            .collect(Collectors.groupingBy(
                Map.Entry::getKey, Collectors.summingDouble(Map.Entry::getValue)
            ));

Take a look at these tutorials provided by Oracle for more information on lambda expressions and streams.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
3

One option is to use the Map.compute method. Its second argument is a remapping function that can be used like in the following example:

Map<String, Double> inputMap1 = Map.of("X1", 5.0, "X2", 3.0);
Map<String, Double> inputMap2 = Map.of("X1", 7.0, "X3", 9.0);
List<Map<String, Double>> listOfMaps = List.of(inputMap1, inputMap2);

Map<String, Double> mergedMap = new HashMap<>();
for (Map<String, Double> map : listOfMaps) {
    map.forEach((key, value) -> {
        mergedMap.compute(key, (k, oldValue) -> oldValue == null ? value : oldValue + value);
    });
}

System.out.println(mergedMap);

oldValue is null if there is not yet an entry with key in mergedMap, and the old value otherwise.

This prints:

{X1=12.0, X2=3.0, X3=9.0}
Michel K
  • 411
  • 1
  • 7
3

Using stream you could do:

ArrayList<HashMap<String, Double>> listHashMaps = // your list of maps

Map<String,Double> mergedMap = 
        listHashMaps.stream()
                .flatMap(m -> m.entrySet().stream())
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, Double::sum));
Eritrean
  • 15,851
  • 3
  • 22
  • 28
2

Update: I might prefer the concise solution by Alexander Ivanchenko. I will leave this Answer posted as an interesting or educational alternative.


Your example data is flawed (repeated keys), so I crafted another set of data.

Map < String, Double > map1 =
        Map.of(
                "X1" , 1d ,
                "X2" , 1d
        );

Map < String, Double > map2 =
        Map.of(
                "X1" , 2d ,
                "X2" , 2d ,
                "X3" , 7d
        );

Define a new Map to hold results.

Map < String, Double > results = new HashMap <>();

Stream & Map#getOrDefault

Process the two input maps by making a stream of each, joining those streams into a single stream. For each map entry in that stream, put the key into the new map, with a value of either the entry's value upon first occurrence or adding the entry's value to the previously put value.

Stream
        .concat( map1.entrySet().stream() , map2.entrySet().stream() )
        .forEach(
                stringDoubleEntry ->
                        results.put(
                                stringDoubleEntry.getKey() ,                                                                        // key
                                results.getOrDefault( stringDoubleEntry.getKey() , 0d ) + stringDoubleEntry.getValue() )  // value
        );

System.out.println( "results.toString() = " + results );

See this code run live at Ideone.com.

map = {X1=3.0, X2=3.0, X3=7.0}

Without streams

If not yet familiar with streams, you could instead use a pair of for-each loops to process each of the two input maps.

Process the first map.

for ( Map.Entry < String, Double > stringDoubleEntry : map1.entrySet() )
{
    String k = stringDoubleEntry.getKey();
    Double v =
            results
                    .getOrDefault( stringDoubleEntry.getKey() , 0d )  // Returns either (A) a value already put into `results` map, or else (B) a default value of zero.
                    + stringDoubleEntry.getValue();
    results.put( k , v );  // Replaces any existing entry (key-value pair) with this pair.
}

And do the same for the second input map. The only difference here is the first line, map1.entrySet() becomes map2.entrySet().

for ( Map.Entry < String, Double > stringDoubleEntry : map2.entrySet() )
{
    String k = stringDoubleEntry.getKey();
    Double v =
            results
                    .getOrDefault( stringDoubleEntry.getKey() , 0d )  // Returns either (A) a value already put into `results` map, or else (B) a default value of zero.
                    + stringDoubleEntry.getValue();
    results.put( k , v );  // Replaces any existing entry (key-value pair) with this pair.
}
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • Thanks for your response. Very well done! I will have to study it a little bit as a lot things are new to me. i assume 0d as 2nd argument for map.getOrDefault is for default value? – redbite May 12 '22 at 19:16
  • 1
    @RedBite The stream concatenation with `forEach` is just a fancy way of doing a pair of for-each loops over each input maps. Perhaps I'll re-do that using conventional code for those not used to streams. – Basil Bourque May 12 '22 at 19:19
  • 1
    @RedBite The `0d` is the default value being specified for the `getOrDefault` method. If no entry is found in our results map for that key, we default to a value of zero. That zero value is being returned by `getOrDefault`. We then immediately add to zero the value of our streamed entry. The result of that addition is finally used as the value to be placed in our results map. – Basil Bourque May 12 '22 at 19:21
2

As others have pointed in the comments, a Map cannot have multiple keys with the same value, so I've associated to each key a List of double values to keep it as close as possible to your example.

To achieve your goal, you could stream the entries of both of your Maps and collect them with the collect(Collectors.toMap()). The keys would stay the same, the content of the Lists would be summed and when keys collide you could simply sum the double values obtained from the first and second colliding List.

Map<String, List<Double>> map1 = Map.of("X1", new ArrayList<>(List.of(1.0, 3.0, 5.0)), "X2", new ArrayList<>(List.of(2.0, 4.0)));
Map<String, List<Double>> map2 = Map.of("X4", new ArrayList<>(List.of(10.0, 7.0)), "X1", new ArrayList<>(List.of(12.0)));

Map<String, Double> mapRes = Stream.concat(map1.entrySet().stream(), map2.entrySet().stream())
        .collect(Collectors.toMap(entry -> entry.getKey(), 
                entry -> entry.getValue().stream().collect(Collectors.summingDouble(Double::doubleValue)), 
                (val1, val2) -> val1 + val2));
Dan
  • 3,647
  • 5
  • 20
  • 26
1

Here is a "You can't teach and old dog new tricks" way of doing it:

public static Map<String, Double> addMapValues 
        (Collection<Map<String, Double>> mapsIn) {
    Map<String, Double> sums = new HashMap<> ();   
    
    for (Map<String, Double> aMap : mapsIn) {
        for (Map.Entry<String, Double> entry: aMap.entrySet()) {
            Double runningTotal = sums.get(entry.getKey());
            if (runningTotal == null) {
                sums.put(entry.getKey(), entry.getValue());
            } else {
                runningTotal += entry.getValue();
                sums.put(entry.getKey(), runningTotal);                    
            }
        }                
    }
    return sums;
}
Old Dog Programmer
  • 901
  • 1
  • 7
  • 9
  • 1
    definitely "You can't teach and old dog new tricks" ahahah!!! this is what i had in mind, but could not code it myself. I have gone with Alexander solution, but thanks for the response - i will try this too. – redbite May 15 '22 at 21:03