Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a map of maps from a list of maps with Java 8 streaming api

Background

I have a list of maps that looks something like this:

[
  {
    "name": "A",
    "old": 0.25,
    "new": 0.3
  },
  {
    "name": "B",
    "old": 0.3,
    "new": 0.35
  },
  {
    "name": "A",
    "old": 0.75,
    "new": 0.7
  },
  {
    "name": "B",
    "old": 0.7,
    "new": 0.60
  }
]

and I want the output to look like this:

{
  "A": {
    "old": 1,
    "new": 1
  },
  "B": {
    "old": 1,
    "new": 0.95
  }
}

...where the values of old and new are summed for each related entry.

The data type of the list of maps is List<Map<String, Object>>, so the output should be a Map<String, Map<String, Double>>.

What I've Tried

With some diagram drawing, documentation reading, and trial and error, I was able to come up with this:

data.stream()
    .collect(
        Collectors.groupingBy(entry -> entry.get("name"),
            Collectors.summingDouble(entry ->
                Double.parseDouble(entry.get("old").toString())))
    );

to produce an object of type Map<String, Double>, where the output is

{
  "A": 1,
  "B": 1
}

for the summations of the old values. However, I can't quite transform it into a map of maps. Something like this:

data.stream()
    .collect(
        Collectors.groupingBy(entry -> entry.get("name"),
            Collectors.mapping(
                Collectors.groupingBy(entry -> entry.get("old"),
                    Collectors.summingDouble(entry ->
                        Double.parseDouble(entry.get("old").toString())
                    )
                ),
                Collectors.groupingBy(entry -> entry.get("new"),
                    Collectors.summingDouble(entry ->
                        Double.parseDouble(entry.get("new").toString())
                    )
                )
            )
        )
    );

doesn't work, because Collectors.mapping() only takes one mapping function and a downstream collector, but I'm not sure how to map two values at once.

Is there another function I need to create mappings of two different values? Any suggestions on better ways of doing this is greatly appreciated as well.

like image 550
homersimpson Avatar asked Oct 04 '18 16:10

homersimpson


People also ask

How do you collect map map in stream?

Method 1: Using Collectors.toMap() Function The Collectors. toMap() method takes two parameters as the input: KeyMapper: This function is used for extracting keys of the Map from stream value. ValueMapper: This function used for extracting the values of the map for the given key.

Can we use map stream in Java 8?

Java 8 Stream's map method is intermediate operation and consumes single element forom input Stream and produces single element to output Stream. It simply used to convert Stream of one type to another. Let's see method signature of Stream's map method.

Can you stream a map in Java?

Converting complete Map<Key, Value> into Stream: This can be done with the help of Map. entrySet() method which returns a Set view of the mappings contained in this map. In Java 8, this returned set can be easily converted into a Stream of key-value pairs using Set. stream() method.


2 Answers

You can use streams, but you can also use Map's computeIfAbsent and merge methods:

Map<String, Map<String, Double>> result = new LinkedHashMap<>();
data.forEach(entry -> {
    String name = (String) entry.get("name");
    Map<String, Double> map = result.computeIfAbsent(name, k -> new HashMap<>());
    map.merge("old", (Double) entry.get("old"), Double::sum);
    map.merge("new", (Double) entry.get("new"), Double::sum);
});
like image 139
fps Avatar answered Oct 07 '22 00:10

fps


It is possible to achieve this only using Stream tools (similar to this):

Map<String, Map<String, Double>> collect = data.stream().collect(
    Collectors.groupingBy(m -> (String)m.get("name"),
    Collector.of(LinkedHashMap::new,
        (acc, e) -> Stream.of("old", "new").forEach(key -> acc.merge(key, (Double) e.get(key), Double::sum)),
        (m1, m2) -> {
          m2.forEach((k, v) -> m1.merge(k, v, Double::sum));
          return m1;
        })
    ));

There is also the > Java 8 way:

Map<String, Map<String, Double>> stats = data.stream().collect(
    Collectors.groupingBy(m -> (String) m.get("name"),
        Collectors.flatMapping(m -> m.entrySet().stream().filter(e -> !"name".equals(e.getKey())),
            Collectors.toMap(Map.Entry::getKey, e -> (Double)e.getValue(), Double::sum, LinkedHashMap::new)
        )
    ));
like image 20
Flown Avatar answered Oct 07 '22 00:10

Flown