Java 8 stream grouping a List<Map<>> by the same <Key, Value> to a new List<Map<>>

Viewed 2363

I have a List<Map<String,String>> such as:

Map<String, String> m1 = new HashMap<>();
m1.put("date", "2020.1.5");
m1.put("B", "10");

Map<String, String> m2 = new HashMap<>();
m2.put("date", "2020.1.5");
m2.put("A", "20");

Map<String, String> m3 = new HashMap<>();
m3.put("date", "2020.1.6");
m3.put("A", "30");

Map<String, String> m4 = new HashMap<>();
m4.put("date", "2020.1.7");
m4.put("C", "30");

List<Map<String, String>> before = new ArrayList<>();
before.add(m1);
before.add(m2);
before.add(m3);
before.add(m4);

My expect result is to generate a new List map, which is grouped by date , and all the entry set in the same date would be put together, like:

[{"A":"20","B":"10","date":"2020.1.5"},{"A":"30","date":"2020.1.6"},{"C":"30","date":"2020.1.7"}]

I tried with the following method, but always not my expect result.

stream().flatmap().collect(Collectors.groupingBy())

Some Additional Comments for this problem:

I worked this out with for LOOP, but the application hangs when the list size is about 50000, so I seek a better performant way to do this. Java 8 stream flat map is a perhaps way as far as I know. So the key point is not only to remap this but also with the most performant way to do this.

4 Answers
before
  .stream()
  .collect(Collectors.toMap((m) -> m.get("date"), m -> m, (a,b) -> {
      Map<String, String> res = new HashMap<>();
      res.putAll(a);
      res.putAll(b);
      return res;
  }))
  .values();

This is the solution you're looking for.

The toMap function receives 3 parameters:

  • the key mapper, which in your case is the date
  • the value mapper, which is the map itself that's being processed
  • the merge function, which takes 2 maps with the same date and puts all the keys together

Output:

[{date=2020.1.5, A=20, B=10}, {date=2020.1.6, A=30}, {date=2020.1.7, C=30}]

You can do this way using groupingBy and Collector.of

List<Map<String, String>> list = new ArrayList<>(before.stream()
        .collect(Collectors.groupingBy(
                k -> k.get("date"),
                Collector.of( HashMap<String,String>::new,
                        (m,e)-> m.putAll(e),
                        (map1,map2)->{ map1.putAll(map2); return map1;}
                ))).values());

Here, first use Collectors.groupingBy to group by date. Then define custom collector using Collector.of to collect List<Map<String, String>> into Map<String, String>. After create list using map values.

And using Collectors.flatMapping from Java 9

List<Map<String, String>> list = new ArrayList<>(before.stream()
        .collect(Collectors.groupingBy(
                k -> k.get("date"),
                Collectors.flatMapping(m -> m.entrySet().stream(), 
                    Collectors.toMap(k -> k.getKey(), v -> v.getValue(), (a,b) -> a))))
               .values());

You can achieve the very same result using a certain number of Collectors, orderly:

  • Collectors.groupingBy to group by the date
  • Collectors.reducing to merge the Map<String, String> items
  • Collectors.collectingAndThen to transform the values from Map<String, Optional<Map<String, String>>>, as a result of the previous reducing to the final output List<Map<String, String>>.
List<Map<String, String>> list = before.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.groupingBy(
            m -> m.get("date"),
            Collectors.reducing((l, r) -> {
                l.putAll(r);
                return l; })
        ),
        o -> o.values().stream()
                       .flatMap(Optional::stream)
                       .collect(Collectors.toList())));

The list contains what are you looking for:

[{date=2020.1.5, A=20, B=10}, {date=2020.1.6, A=30}, {date=2020.1.7, C=30}]

Important: This solution has two he disadvantages:

  • It looks clumsy and might not be clear for an independent viewer
  • It mutates (modifies) the original maps included in the List<Map<String, String>> before.

It can be done as follows:

List<Map<String, String>> remapped = before.stream()
    .collect(Collectors.groupingBy(m -> m.get("date")))
    .values().stream()
    .map(e -> e.stream()
               .flatMap(m -> m.entrySet().stream())
               .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (x1, x2) -> x1)))
    .collect(Collectors.toList());

remapped.forEach(System.out::println);

Output:

{date=2020.1.5, A=20, B=10}
{date=2020.1.6, A=30}
{date=2020.1.7, C=30}
Related