4

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?

6
  • Would HashMap <String, ArrayList <Double>> mergedMap work for you? Commented May 12, 2022 at 18:40
  • @James It should be Map<String, List<Double>>, not HashMap and ArrayList. Commented May 12, 2022 at 18:41
  • If you were using Guava MultiMaps, this Q&A might help: stackoverflow.com/questions/51526988/merge-two-guava-multimaps Commented May 12, 2022 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! Commented May 12, 2022 at 19:05
  • 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. Commented May 15, 2022 at 21:01

6 Answers 6

6

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.

Sign up to request clarification or add additional context in comments.

Comments

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

Comments

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}

Comments

3

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.
}

3 Comments

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 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.
@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.
2

To merge two maps by keeping the original entries, while summing the values with same keys, you could:

  1. concat() the streams of the maps' entries.
  2. Collect with the overloaded method Collectors.toMap() to map each value with their original key, while combining values with colliding keys with a merge function (in your case, the method reference Double::sum).
Stream.concat(map1.entrySet().stream(), map2.entrySet().stream())
    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, Double::sum));

Comments

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

1 Comment

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.

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.