5

I have a list of nested maps (List<Map<String, Map<String, Long>>>) and the goal is to reduce the list to a single map, and the merging is to be done as follow: if map1 contains x->{y->10, z->20} and map2 contains x->{y->20, z->20} then these two should be merged to x->{y->30, z->40}.

I tried to do it as follow and this is working fine.

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.BinaryOperator;
import java.util.stream.Collectors;

public class Test {
    public static void main(String args[]) throws IOException {
        Map<String, Map<String, Long>> data1 = new HashMap<>();
        Map<String, Long> innerData1 = new HashMap<>();
        innerData1.put("a", 10L);
        innerData1.put("b", 20L);
        data1.put("x", innerData1);
        Map<String, Long> innerData2 = new HashMap<>();
        innerData2.put("b", 20L);
        innerData2.put("a", 10L);
        data1.put("x", innerData1);

        Map<String, Map<String, Long>> data2 = new HashMap<>();
        data2.put("x", innerData2);

        List<Map<String, Map<String, Long>>> mapLists = new ArrayList<>();
        mapLists.add(data1);
        mapLists.add(data2);

        Map<String, Map<String, Long>>  result  = mapLists.stream().flatMap(map -> map.entrySet().stream()).
        collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, new BinaryOperator<Map<String, Long>>() {

            @Override
            public Map<String, Long> apply(Map<String, Long> t,
                    Map<String, Long> u) {
                Map<String, Long> result = t;
                for(Entry<String, Long> entry: u.entrySet()) {
                    Long val = t.getOrDefault(entry.getKey(), 0L);
                    result.put(entry.getKey(), val + entry.getValue());
                }
                return result;
            }
        }));
    }
}

Is there any other better and efficient approach to solve this?

How to do it more cleanly if the nesting level is more than 2? Suppose the List is like List<Map<String, Map<String, Map<String, Long>>>> and we have to reduce it to a single Map<String, Map<String, Map<String, Long>>>, assuming similar merge functionality as above.

Tunaki
  • 132,869
  • 46
  • 340
  • 423
Mukesh
  • 283
  • 3
  • 11

2 Answers2

4

You have the general idea, it is just possible to simplify a little the process of merging two maps together. Merging the two maps can be done easily with:

Map<String, Integer> mx = new HashMap<>(m1);
m2.forEach((k, v) -> mx.merge(k, v, Long::sum));

This code creates the merged map mx from m1, then iterates over all entries of the second map m2 and merges each entry into mx with the help of Map.merge(key, value, remappingFunction): this method will add the given key with the given value if no mapping existed for that key, else it will remap the existing value for that key and the given value with the given remapping function. In our case, the remapping function should sum the two values together.

Code:

Map<String, Map<String, Long>> result  =
    mapLists.stream()
            .flatMap(m -> m.entrySet().stream())
            .collect(Collectors.toMap(
                Map.Entry::getKey,
                Map.Entry::getValue, 
                (m1, m2) -> { 
                    Map<String, Long> mx = new HashMap<>(m1);
                    m2.forEach((k, v) -> mx.merge(k, v, Long::sum));
                    return mx;
                }
            ));

If there are more "levels", you could define a merge method:

private static <K, V> Map<K, V> merge(Map<K, V> m1, Map<K, V> m2, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
    Map<K, V> mx = new HashMap<>(m1);
    m2.forEach((k, v) -> mx.merge(k, v, remappingFunction));
    return mx;
}

and use it recursively. For example, to merge two Map<String, Map<String, Long>> m1 and m2, you could use

merge(m1, m2, (a, b) -> merge(a, b, Long::sum));

as the remapping function is Collectors.toMap.

Community
  • 1
  • 1
Tunaki
  • 132,869
  • 46
  • 340
  • 423
2

Using my StreamEx library:

Map<String, Map<String, Long>> result = StreamEx.of(mapLists)
        .flatMapToEntry(m -> m)
        .toMap((m1, m2) -> EntryStream.of(m1).append(m2).toMap(Long::sum));

The flatMapToEntry intermediate operation flattens maps into EntryStream<String, Map<String, Long>> which extends Stream<Map.Entry<String, Map<String, Long>>>. The toMap terminal operation just creates a map from the stream of entries using the supplied merge function. To merge two maps we use EntryStream again.

Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334