4

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.

Eklavya
  • 17,618
  • 4
  • 28
  • 57
Better Man
  • 51
  • 1
  • 5
  • 5
    I have a question about your expected result, do you really want the date key and the date value in the same map as your values? why can you not have a `Map>` where the first map is keyed on date, and the `List>` is your actual values? – scigs Jun 15 '20 at 14:11
  • I'll also ask, why do you need to do this with java 8 streams? I doubt it would be more performant, and you could definitely do it in a more readable way just by hand-rolling the steps through the code. – BeUndead Jun 15 '20 at 14:16
  • @BeUndead not sure why you think a Java 8 solution is not readable. Not so performant, perhaps. But definitely not ugly code. – Silviu Burcea Jun 15 '20 at 14:34
  • @scigs, actually a list is indeed my expect return, not a map – Better Man Jun 15 '20 at 14:50
  • @BeUndead, before asking this question, I tried the for loop to do this. But the application hang when the List size is about 50 thousand. As far as I know, Java8 stream has a better performance to do this. So I ask this question. Correct me if I have a wrong conception. – Better Man Jun 15 '20 at 14:54
  • @SilviuBurcea, why Java8 is not so performant? Is there any better performant way to handle a huge size of this type of list? – Better Man Jun 15 '20 at 14:58
  • Well, when you have 2 or more entries for the same date, you have to create a new map to hold (intermediate) results of merging maps together. There would be n - 1 maps created (e.g. one to hold results for 2 maps, 2 for merging 3 maps, one for first 2 maps and then another for merging that intermediate results with the 3rd map). You can probably have a better Java 8 implementation if you flatMap the key/value entries of each map and put those values once in a final map for a given date. – Silviu Burcea Jun 15 '20 at 15:13
  • @SilviuBurcea: This was a guess as to how the implementations would look (and a quick glance at the answers so far would support this). I can't really imagine anyone finding a stream solution _more_ readable than a `for` loop achieving the same - perhaps _as_ readable with extreme familiarity with java 8 streams, but that's rare to come by. – BeUndead Jun 15 '20 at 15:20
  • @BeUndead I find all the solutions pretty readable. If one wants to improve the code readability, functions could be named, e.g. `Function> classifier = (map) -> map.get("date");` and use it `Collectors.groupingBy(classifier, ...)` instead of inlining it. I agree that some things are not as readable when you first read some lambda code but if you've seen lambda code before, all solutions look good. You can't go simpler than that and I don't think a loop does much better. – Silviu Burcea Jun 15 '20 at 16:22
  • @SilviuBurcea, do you mean the flat map is the most performant way to handle this even though the list size is too big? – Better Man Jun 16 '20 at 02:20
  • @BetterMan I've checked Eklavya's answer and it appears to be working without creating more maps than needed, just one per date, which is optimal, you can't achieve better than this. – Silviu Burcea Jun 16 '20 at 05:02
  • @BetterMan I suggest you pick an answer and accept it if it solved your problem. – Silviu Burcea Jun 19 '20 at 10:21
  • 1
    @SilviuBurcea, already done this. I'm just back from a serious problem. Sorry for the late reply. – Better Man Jul 01 '20 at 05:13

4 Answers4

7
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}]
Silviu Burcea
  • 5,103
  • 1
  • 29
  • 43
4

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());
Eklavya
  • 17,618
  • 4
  • 28
  • 57
1

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.
Nikolas Charalambidis
  • 40,893
  • 16
  • 117
  • 183
0

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}
Eklavya
  • 17,618
  • 4
  • 28
  • 57
Nowhere Man
  • 19,170
  • 9
  • 17
  • 42