优雅的方式在groupingBy中对Set的Set进行flatMap

6
所以我有一段代码,我正在遍历一个数据列表。每个数据都是一个包含一个Long caseId和一个RulingReportData。每个Ruling都有一个或多个Payment。我想要一个Map,其中caseId作为键,支付集合作为值(即一个Map<Long,Set<Payments>>)。
案例在行内不唯一,但是案例是唯一的。
换句话说,我可以有几行具有相同案例的数据,但它们将具有唯一的裁决。
以下代码可以让我获得一个Map<Long,Set<Set<Payments>>>,这几乎是我想要的,但我一直在努力寻找正确的方式来在给定上下文中对最终集进行flatMap。我一直在使用这个映射来解决逻辑问题,但我非常希望修复算法,以便将支付集合正确组合成一个单一的集合,而不是创建一个集合的集合。
我搜索了一下,没有找到与相同迭代类型的问题,虽然使用Java流进行flatMap似乎是一个比较流行的主题。
rowData.stream()
        .collect(Collectors.groupingBy(
            r -> r.case.getCaseId(),
            Collectors.mapping(
                r -> r.getRuling(),
                Collectors.mapping(ruling->
                    ruling.getPayments(),
                    Collectors.toSet()
                )
            )));

2
在“ReportData”列表中,caseIds是否唯一? - marstran
不,它们并不是唯一的。输入数据包含重复的案例,但它们都有针对该特定案例的独特裁决。因此,你可以说输入数据并不理想 :P - Lars Holdaas
2
你能提供涉及到的类的代码吗?(ReportData, Ruling, Payment) 这将非常清楚地说明每个类是什么。另外,你提供的代码显然无法编译:有一个被引用的case字段(保留关键字),而且似乎r.getRulings()应该是r.getRuling() - Didier L
@DidierL 没错,我把 getRulings() 编辑成了 getRuling()。至于这个 case,我只是快速翻译了字段名称(在这个项目中我们不使用英语作为字段名称等)。你说得对,它是一个保留关键字,不能使用。 - Lars Holdaas
1
下次,请提供一个[mcve]。在这里,我不得不根据我理解的问题来猜测所有的类结构,才能在我的IDE中工作。我认为@marstran只是在行内编写答案,但他肯定无法验证它。 - Didier L
3个回答

5

另一个 JDK8 的解决方案:

Map<Long, Set<Payment>> resultSet = 
         rowData.stream()
                .collect(Collectors.toMap(p -> p.Case.getCaseId(),
                        p -> new HashSet<>(p.getRuling().getPayments()),
                        (l, r) -> { l.addAll(r);return l;}));

从JDK9开始,您可以使用flatMapping收集器:

rowData.stream()
       .collect(Collectors.groupingBy(r -> r.Case.getCaseId(), 
              Collectors.flatMapping(e -> e.getRuling().getPayments().stream(), 
                        Collectors.toSet())));

@DidierL 是的,考虑到之前的编辑是正确的。最近Java有点生疏了。谢谢。 - Ousmane D.
你应该保留 new HashSet() 包装器,否则你会修改源集合中的 ruling 支付。 - Didier L
@DidierL 好的,那就保留它吧。;-) - Ousmane D.
1
这似乎是正确的方式。 - Lars Holdaas
2
如果你仍然被迫使用Java 8,你可以在这个答案的末尾找到一个兼容Java 8的flatMapping收集器的实现。由于它具有相同的签名,因此将来很容易从该解决方案迁移到标准API版本。 - Holger

2
最干净的解决方案是定义自己的收集器:
Map<Long, Set<Payment>> result = rowData.stream()
        .collect(Collectors.groupingBy(
                ReportData::getCaseId,
                Collector.of(HashSet::new,
                        (s, r) -> s.addAll(r.getRuling().getPayments()),
                        (s1, s2) -> { s1.addAll(s2); return s1; })
        ));

我曾经想到的两个解决方案实际上不够高效和易读,但仍然避免了构建中间的Map

使用Collectors.reducing()合并内部集合:

Map<Long, Set<Payment>> result = rowData.stream()
        .collect(Collectors.groupingBy(
                ReportData::getCaseId,
                Collectors.reducing(Collections.emptySet(),
                        r -> r.getRuling().getPayments(),
                        (s1, s2) -> {
                            Set<Payment> r = new HashSet<>(s1);
                            r.addAll(s2);
                            return r;
                        })
        ));

这里的reducing操作将合并具有相同caseId的条目的Set<Payment>。然而,如果需要合并很多次,这可能会导致大量副本集。

另一个解决方案是使用下游收集器,对嵌套集合进行flatmap处理:

Map<Long, Set<Payment>> result = rowData.stream()
        .collect(Collectors.groupingBy(
                ReportData::getCaseId,
                Collectors.collectingAndThen(
                        Collectors.mapping(r -> r.getRuling().getPayments(), Collectors.toList()),
                        s -> s.stream().flatMap(Set::stream).collect(Collectors.toSet())))
        );

基本上,它将所有匹配的caseId集合放在一起放入一个List中,然后将该列表扁平化为单个Set


1

可能有更好的方法来做这件事,但这是我找到的最好的方法:

 Map<Long, Set<Payment>> result =
            rowData.stream()
                    // First group by caseIds.
                    .collect(Collectors.groupingBy(r -> r.case.getCaseId()))
                    .entrySet().stream()
                    // By streaming over the entrySet, I map the values to the set of payments.
                    .collect(Collectors.toMap(
                            Map.Entry::getKey,
                            entry -> entry.getValue().stream()
                                    .flatMap(r -> r.getRuling().getPayments().stream())
                                    .collect(Collectors.toSet())));

哦,糟糕,我意识到我之前提供的信息是错误的。实际上每行只有一个裁决结果。我搞混了,因为输入数据本身包含了重复的案例,但每个数据行只有一个案例和一个裁决结果。非常抱歉! - Lars Holdaas
1
@LarsHoldaas 好的,我编辑了答案,每个案例只有一个规则。想法应该是一样的,对吧? - marstran
哦耶!这似乎像魔法一样运作!非常感谢 :D - Lars Holdaas
1
如果你使用的是Java 9,请查看@Aomine的答案以获取更好的解决方案。通过使用flatMapping收集器,您不再需要从groupingBy获得立即映射。 - marstran

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