Search code examples
javahashmapjava-streamcollectors

How to merge List of Maps of Maps into a Map of Maps?


Could you help me with Java Streams?

As you can see from the title I need to merge List<Map<String, Map<String, Genuineness>>> into Map<String, Map<String, Genuineness>>.

The list is represented as List<Map<String, Map<String, Genuineness>>> and looks like:

[  
   {  
      "USER_1":{  
         "APP_1":{  
            "total":1,
            "totalGenuine":1,
            "totalDevelopment":1
         }
      },
      "USER_2":{  
         "APP_1":{  
            "total":1,
            "totalGenuine":1,
            "totalDevelopment":1
         },
         "APP_2":{  
            "total":2,
            "totalGenuine":2,
            "totalDevelopment":2
         }
      }
   },
   {  
      "USER_1":{  
         "APP_1":{  
            "total":1,
            "totalGenuine":1,
            "totalDevelopment":1
         }
      },
      "USER_2":{  
         "APP_1":{  
            "total":1,
            "totalGenuine":1,
            "totalDevelopment":1
         },
         "APP_2":{  
            "total":2,
            "totalGenuine":2,
            "totalDevelopment":2
         }
      }
   }
]

So, as you can see, duplicate keys could be everywhere. My goal is to combine them into Map<String, Map<String, Genuineness>> by merging Genuineness. Merge Genuineness simply means return a new object with summed up values total, totalGenuine, and totalDevelopment.

Here is my implementation that fails:

final Map<String, Map<String, Genuineness>> map = taskHandles.stream().map(this::mapTaskHandle)
                .flatMap(m -> m.entrySet().stream()).collect(
                        Collectors.toMap(Map.Entry::getKey, e -> e.getValue().entrySet().stream()
                                .collect(
                                        Collectors.toMap(Map.Entry::getKey,
                                                g -> new Genuineness(g.getValue().getTotal(), g.getValue().getTotalGenuine(), g.getValue().getTotalDevelopment()),
                                                (g1, g2) -> new Genuineness(g1.getTotal() + g2.getTotal(),
                                                        g1.getTotalGenuine() + g2.getTotalGenuine(),
                                                        g1.getTotalDevelopment() + g2.getTotalGenuine()
                                                )
                                        )
                                )
                        )
                );

It fails with message:

java.lang.IllegalStateException: Duplicate key {TEST_33_33_APP_1=live.attach.billing.domain.model.billing.Genuineness@951b6fe}

So, seems that in my implementation I've pointed how to combine inner map but didn't make a merging of values of outer map and I don't know how to do it.

I greatly appreciate your help. Thank you in advance!

UPDATE: Expected output:

   {  
      "USER_1":{  
         "APP_1":{  
            "total":2,
            "totalGenuine":2,
            "totalDevelopment":2
         }
      },
      "USER_2":{  
         "APP_1":{  
            "total":2,
            "totalGenuine":2,
            "totalDevelopment":2
         },
         "APP_2":{  
            "total":4,
            "totalGenuine":4,
            "totalDevelopment":4
         }
      }
   }

Solution

  • Honestly, this is a horrible data structure to work with and the maintainers of this code will have a hard time finding out problems that arise.

    You should take a step back and consider refactoring the code, any way you can solve the missing part by using the following merge function in the outmost toMap:

    (l, r) -> {
          r.forEach((k, v) -> l.merge(k, v,
                        (bi, bii) -> new Genuineness(bi.getTotal() + bii.getTotal(),
                                   bi.getTotalGenuine() + bii.getTotalGenuine(),
                                   bi.getTotalDevelopment() + bii.getTotalGenuine())));
           return l;
    }
    

    Full code:

    taskHandles.stream().map(this::mapTaskHandle)
                    .flatMap(m -> m.entrySet().stream()).collect(
                            Collectors.toMap(Map.Entry::getKey, e -> e.getValue().entrySet().stream()
                                    .collect(
                                            Collectors.toMap(Map.Entry::getKey,
                                                    g -> new Genuineness(g.getValue().getTotal(), g.getValue().getTotalGenuine(), g.getValue().getTotalDevelopment()),
                                                    (g1, g2) -> new Genuineness(g1.getTotal() + g2.getTotal(),
                                                            g1.getTotalGenuine() + g2.getTotalGenuine(),
                                                            g1.getTotalDevelopment() + g2.getTotalGenuine()
                                                    )
                                            )
                                    ),(l, r) -> {
                                      r.forEach((k, v) -> l.merge(k, v,
                                              (bi, bii) -> new Genuineness(bi.getTotal() + bii.getTotal(),
                                                      bi.getTotalGenuine() + bii.getTotalGenuine(),
                                                      bi.getTotalDevelopment() + bii.getTotalGenuine())));
                                      return l;
                                    }
    
                            )
                    );
    

    Ideone