Java 8 如何将一个 List<Map<>> 按照相同的 <Key, Value> 分组成一个新的 List<Map<>>?

4

我有一个 List<Map<String,String>> ,例如:

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);

我的期望结果是生成一个新的List map,该map按日期分组,同一日期中的所有条目集将被放在一起,例如:

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

我尝试了以下方法,但总是没有达到我的预期结果。

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

针对此问题的一些额外评论:

我使用了 for 循环来解决这个问题,但当列表大小达到约 50000 时,应用程序会挂起,因此我寻求更好的性能方法。据我所知,Java 8 流 flat map 可能是一种方式。 因此,关键点不仅在于重新映射,还要以最高效的方式完成。


5
关于您期望的结果,我有一个问题,您真的希望日期键和日期值与实际值一起在同一个映射中吗?为什么不可以使用Map<String, List<Map<String, String>>,其中第一个映射以日期为键,而List<Map<>>是您的实际值? - scigs
在提出这个问题之前,我尝试使用for循环来完成。但是当List大小约为五万时,应用程序会挂起。据我所知,Java8流具有更好的性能来完成此操作。因此我提出了这个问题。如果我有错误的概念,请纠正我。 - Better Man
@BeUndead 我认为所有的解决方案都很易读。如果想要提高代码可读性,可以给函数命名,例如 Function<Map<String, String>> classifier = (map) -> map.get("date"); 并使用 Collectors.groupingBy(classifier, ...) 而不是内联它。我同意有些 lambda 代码在第一次阅读时不太容易理解,但如果你之前看过 lambda 代码,所有的解决方案都很好。你不能比这更简单了,我认为循环也做不到更好。 - Silviu Burcea
@SilviuBurcea,你的意思是即使列表很大,使用flat map仍然是处理这个问题最高效的方法吗? - Better Man
1
@SilviuBurcea,已经完成了。我刚刚解决了一个严重的问题。抱歉回复晚了。 - Better Man
显示剩余8条评论
4个回答

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();

这是您要寻找的解决方案。

toMap函数接收三个参数:

  • 键映射器,您的情况下为日期
  • 值映射器,它是正在处理的地图本身
  • 合并函数,它将具有相同日期的2个地图放在一起

输出:

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

4
你可以使用groupingByCollector.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());

首先使用Collectors.groupingBy按日期进行分组,然后使用Collector.of定义自定义收集器将List<Map<String, String>> 收集到 Map<String, String> 中。接着使用地图值创建列表。

并且在Java 9中使用Collectors.flatMapping

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());

1
您可以使用一定数量的收集器按顺序实现完全相同的结果:
  • Collectors.groupingBy 按日期分组
  • Collectors.reducing 合并 Map<String, String> 项目
  • Collectors.collectingAndThen 将值从 Map<String, Optional<Map<String, String>>> 转换为上一个 reducing 的最终输出 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())));

list包含你所寻找的内容:

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

重要提示:这种解决方案有两个缺点:

  • 它看起来笨拙,可能不清晰,对于独立的观众而言;
  • 它会改变在List<Map<String, String>>中包含的原始映射。

3
警告:此解决方案会对原地图进行更改! - Silviu Burcea
我已经包含了注释。谢谢。 - Nikolas Charalambidis

0
可以按照以下方式完成:
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);

输出:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接