Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reducing list of two level maps to a single two level map using java 8 stream operators

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.

like image 818
Mukesh Avatar asked Jan 04 '16 20:01

Mukesh


People also ask

What does reduce () method does in stream?

Reducing is the repeated process of combining all elements. reduce operation applies a binary operator to each element in the stream where the first argument to the operator is the return value of the previous application and second argument is the current stream element.

What is the purpose of map () method of stream in Java 8?

map. Returns a stream consisting of the results of applying the given function to the elements of this stream. This is an intermediate operation.

What is the use of reduce in Java 8?

In Java, reducing is a terminal operation that aggregates a stream into a type or a primitive type. Java 8 provides Stream API contains set of predefined reduction operations such as average(), sum(), min(), max(), and count(). These operations return a value by combining the elements of a stream.

What is the difference between map and reduce in Java 8?

The map() function returns a new array through passing a function over each element in the input array. This is different to reduce() which takes an array and a function in the same way, but the function takes 2 inputs - an accumulator and a current value.


1 Answers

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.

like image 107
Tunaki Avatar answered Oct 15 '22 21:10

Tunaki