【问题标题】:Using groupingBy into a nested Map, but collecting to a different type of object在嵌套地图中使用 groupingBy,但收集到不同类型的对象
【发布时间】:2017-05-17 22:28:56
【问题描述】:

所以我有这个“有效”的代码(为简单起见替换了一些名称):

 Map<String, Map<String, ImmutableList<SomeClassA>>> someMap =
      someListOfClassA.stream()
      .filter(...)
      .collect(Collectors.groupingBy(SomeClassA::someCriteriaA,
            Collectors.groupingBy(SomeClassA::someCriteriaB, GuavaCollectors.toImmutableList()
            )
      ));

但是,我想更改此代码,以便在按 SomeClassA 字段分组后,内部集合属于 SomeClassB。例如,如果类如下所示:

假设它们都有所有 args 构造函数

class SomeClassA { 
    String someCriteriaA;
    String someCriteriaB;
    T someData;
    String someId;
}

class SomeClassB {
    T someData;
    String someId; 
}

还有一个方法:

public static Collection<SomeClassB> getSomeClassBsFromSomeClassA(SomeClassA someA) {
    List<Some List of Class B> listOfB = someMethod(someA);
    return listOfB; // calls something using someClassA, gets a list of SomeClassB 
}

我想将 SomeClass B 的结果列表展平为

Map<String, Map<String, ImmutableList<SomeClassB>>> someMap = 
    someListOfClassA.stream()
    .filter(...)
    . // not sure how to group by SomeClassA fields but result in a list of SomeClassB since one SomeClassA can result in multiple SomeClassB

我不确定这是否适合上面的代码。如何将一堆基于 SomeClassB 的列表收集到 SomeClassA 的所有值的单个列表中?如果单个 ClassA 映射到单个 ClassB,我知道如何使用 Collectors.mapping 让它工作,但由于每个 ClassA 都会导致多个 ClassB,我不知道如何让它工作。

任何想法将不胜感激。谢谢!

【问题讨论】:

标签: java java-8 java-stream guava


【解决方案1】:

使用这样的自定义收集器:

private static Collector<Collection<SomeClassB>, ?, ImmutableList<SomeClassB>>
        flatMapToImmutableList() {
        return Collectors.collectingAndThen(Collectors.toList(),
                listOfCollectionsOfB ->
                        listOfCollectionsOfB.stream()
                                .flatMap(Collection::stream)
                                .collect(GuavaCollectors.toImmutableList()));
    }

你可以实现你所追求的:

Map<String, Map<String, List<SomeClassB>>> someMap =
                someListOfClassA.stream()
                        .filter(...)
                        .collect(Collectors.groupingBy(SomeClassA::getSomeCriteriaA,
                                Collectors.groupingBy(SomeClassA::getSomeCriteriaB,
                                        Collectors.mapping(a -> getSomeClassBsFromSomeClassA(a),
                                                flatMapToImmutableList()))));

【讨论】:

    【解决方案2】:

    虽然我们都在等待 Java 9 的 Collectors.flatMapping(感谢 @shmosel 提供的链接),但您可以编写自己的收集器来实现您想要的:

    public static <T, D, R> Collector<T, ?, R> flatMapping(
            Function<? super T, ? extends Stream<? extends D>> streamMapper,
            Collector<? super D, ?, R> downstream) {
    
        class Acc {
            Stream.Builder<Stream<? extends D>> builder = Stream.builder();
    
            void add(T t) {
                builder.accept(streamMapper.apply(t));
            }
    
            Acc combine(Acc another) {
                another.builder.build().forEach(builder);
                return this;
            }
    
            R finish() {
                return builder.build()
                        .flatMap(Function.identity()) // Here!
                        .collect(downstream);
            }
        }
        return Collector.of(Acc::new, Acc::add, Acc::combine, Acc::finish);
    }
    

    此辅助方法使用Collector.of 和本地类Acc 来累积由提供的streamMapper 函数返回的流,该函数将原始流的元素作为参数。这些流累积在Stream.Builder 中,当应用收集器的整理器功能时将构建该Stream.Builder

    在构建此流流之后,立即将其与恒等函数进行平面映射,因为我们只想连接流。 (我本可以使用流列表而不是流流,但我认为Stream.Builder 既非常有效又未被充分利用)。

    Acc 还实现了一个组合器方法,它将给定的Acc 的流流合并到这个Acc 的流构建器中。仅当原始流是并行的时才会使用此功能。

    以下是如何在您的示例中使用此方法:

    Map<String, Map<String, ImmutableList<SomeClassB>>> map = someListOfClassA.stream()
        .filter(...)
        .collect(
            Collectors.groupingBy(SomeClassA::getSomeCriteriaA,
                Collectors.groupingBy(SomeClassA::getSomeCriteriaB,
                    flatMapping(
                        a -> getSomeClassBsFromSomeClassA(a).stream(),
                        ImmutableList.toImmutableList()))));
    

    编辑:正如@Holger 在下面的 cmets 中指出的那样,在累积时不需要将数据缓冲到流构建器中。相反,可以通过在累加器函数中执行展平权来实现展平映射收集器。 Here is @Holger's own implementation of such collector,在他同意的情况下,我在这里逐字复制:

    public static <T, U, A, R> Collector<T, ?, R> flatMapping(
            Function<? super T, ? extends Stream<? extends U>> mapper,
            Collector<? super U, A, R> downstream) {
    
        BiConsumer<A, ? super U> acc = downstream.accumulator();
        return Collector.of(downstream.supplier(),
                (a, t) -> {
                    try (Stream<? extends U> s = mapper.apply(t)) {
                        if (s != null) s.forEachOrdered(u -> acc.accept(a, u));
                    }
                },
                downstream.combiner(), downstream.finisher(),
                downstream.characteristics().toArray(new Collector.Characteristics[0]));
    }
    

    【讨论】:

    • 不需要将数据缓冲到构建器中。 flatMapping 收集器可以通过在累加器函数中执行展平权来实现。末尾this answer就是这样一个实现。它不仅更高效,而且更简单,同时还关心关闭策略。它与 Java 9 的实现相当(如果不相同)。
    • @Holger 你是绝对正确的,在你的版本中,平面映射是在流终止时隐式执行的(通过将其元素累积到下游收集器中)。您是否允许我链接到您的答案并逐字复制您的平面映射收集器,让读者清楚地知道收集器是您的?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-06-22
    • 2019-12-26
    • 1970-01-01
    • 1970-01-01
    • 2014-04-10
    • 2021-02-12
    • 2020-11-16
    相关资源
    最近更新 更多