首页 文章

Java 8 Stream:使用多个收集器进行分组

提问于
浏览
10

我想通过一个分类器使用Java 8 Stream和Group,但是有多个Collector函数 . 因此,在分组时,例如计算一个场(或可能是另一个场)的平均值和总和 .

我试着用一个例子来简化这个:

public void test() {
    List<Person> persons = new ArrayList<>();
    persons.add(new Person("Person One", 1, 18));
    persons.add(new Person("Person Two", 1, 20));
    persons.add(new Person("Person Three", 1, 30));
    persons.add(new Person("Person Four", 2, 30));
    persons.add(new Person("Person Five", 2, 29));
    persons.add(new Person("Person Six", 3, 18));

    Map<Integer, Data> result = persons.stream().collect(
            groupingBy(person -> person.group, multiCollector)
    );
}

class Person {
    String name;
    int group;
    int age;

    // Contructor, getter and setter
}

class Data {
    long average;
    long sum;

    public Data(long average, long sum) {
        this.average = average;
        this.sum = sum;
    }

    // Getter and setter
}

结果应该是一个将分组结果关联起来的Map

1 => Data(average(18, 20, 30), sum(18, 20, 30))
2 => Data(average(30, 29), sum(30, 29))
3 => ....

这与“Collectors.counting()”之类的函数完美配合,但我喜欢链接多个(理想情况下,List中的无限) .

List<Collector<Person, ?, ?>>

有可能做这样的事吗?

4 回答

  • 3

    对于求和和求平均的具体问题,请使用collectingAndThensummarizingDouble

    Map<Integer, Data> result = persons.stream().collect(
            groupingBy(Person::getGroup, 
                    collectingAndThen(summarizingDouble(Person::getAge), 
                            dss -> new Data((long)dss.getAverage(), (long)dss.getSum()))));
    

    对于更通用的问题(收集关于你人员的各种事情),你可以创建一个像这样的复杂收集器:

    // Individual collectors are defined here
    List<Collector<Person, ?, ?>> collectors = Arrays.asList(
            Collectors.averagingInt(Person::getAge),
            Collectors.summingInt(Person::getAge));
    
    @SuppressWarnings("unchecked")
    Collector<Person, List<Object>, List<Object>> complexCollector = Collector.of(
        () -> collectors.stream().map(Collector::supplier)
            .map(Supplier::get).collect(toList()),
        (list, e) -> IntStream.range(0, collectors.size()).forEach(
            i -> ((BiConsumer<Object, Person>) collectors.get(i).accumulator()).accept(list.get(i), e)),
        (l1, l2) -> {
            IntStream.range(0, collectors.size()).forEach(
                i -> l1.set(i, ((BinaryOperator<Object>) collectors.get(i).combiner()).apply(l1.get(i), l2.get(i))));
            return l1;
        },
        list -> {
            IntStream.range(0, collectors.size()).forEach(
                i -> list.set(i, ((Function<Object, Object>)collectors.get(i).finisher()).apply(list.get(i))));
            return list;
        });
    
    Map<Integer, List<Object>> result = persons.stream().collect(
            groupingBy(Person::getGroup, complexCollector));
    

    映射值是列表,其中第一个元素是应用第一个收集器的结果,依此类推 . 您可以使用 Collectors.collectingAndThen(complexCollector, list -> ...) 添加自定义修整器步骤,以将此列表转换为更合适的列表 .

  • 15

    通过使用 Map 作为输出类型,可以有一个潜在的无限减速器列表,每个减速器都会生成自己的统计数据并将其添加到 Map 中 .

    public static <K, V> Map<K, V> addMap(Map<K, V> map, K k, V v) {
        Map<K, V> mapout = new HashMap<K, V>();
        mapout.putAll(map);
        mapout.put(k, v);
        return mapout;
    }
    

    ...

    List<Person> persons = new ArrayList<>();
        persons.add(new Person("Person One", 1, 18));
        persons.add(new Person("Person Two", 1, 20));
        persons.add(new Person("Person Three", 1, 30));
        persons.add(new Person("Person Four", 2, 30));
        persons.add(new Person("Person Five", 2, 29));
        persons.add(new Person("Person Six", 3, 18));
    
        List<BiFunction<Map<String, Integer>, Person, Map<String, Integer>>> listOfReducers = new ArrayList<>();
    
        listOfReducers.add((m, p) -> addMap(m, "Count", Optional.ofNullable(m.get("Count")).orElse(0) + 1));
        listOfReducers.add((m, p) -> addMap(m, "Sum", Optional.ofNullable(m.get("Sum")).orElse(0) + p.i1));
    
        BiFunction<Map<String, Integer>, Person, Map<String, Integer>> applyList
                = (mapin, p) -> {
                    Map<String, Integer> mapout = mapin;
                    for (BiFunction<Map<String, Integer>, Person, Map<String, Integer>> f : listOfReducers) {
                        mapout = f.apply(mapout, p);
                    }
                    return mapout;
                };
        BinaryOperator<Map<String, Integer>> combineMaps
                = (map1, map2) -> {
                    Map<String, Integer> mapout = new HashMap<>();
                    mapout.putAll(map1);
                    mapout.putAll(map2);
                    return mapout;
                };
        Map<String, Integer> map
                = persons
                .stream()
                .reduce(new HashMap<String, Integer>(),
                        applyList, combineMaps);
        System.out.println("map = " + map);
    

    产品:

    map = {Sum=10, Count=6}
    
  • 4

    您应该构建一个抽象,它是收集器的聚合器,而不是链接收集器:使用接受收集器列表的类实现 Collector 接口,并将每个方法调用委托给每个收集器 . 然后,最后,返回 new Data() ,其中包含嵌套收集器生成的所有结果 .

    您可以通过使用 Collector.of(supplier, accumulator, combiner, finisher, Collector.Characteristics... characteristics) 来避免使用所有方法声明创建自定义类. finisher lambda将调用每个嵌套收集器的整理器,然后返回 Data 实例 .

  • 3

    你可以链接他们,

    收集器只能生成一个对象,但此对象可以包含多个值 . 例如,您可以返回一个Map,其中 Map 为您要返回的每个收集器都有一个条目 .

    你可以使用 Collectors.of(HashMap::new, accumulator, combiner);

    您的 accumulator 将有一个收集器 Map ,其中生成的 Map 的键与收集器的名称相匹配 . 当并行执行时,组合器需要一种方法来组合多个结果esp .


    通常,内置收集器使用数据类型来获得复杂的结果 .

    来自收藏家

    public static <T>
    Collector<T, ?, DoubleSummaryStatistics> summarizingDouble(ToDoubleFunction<? super T> mapper) {
        return new CollectorImpl<T, DoubleSummaryStatistics, DoubleSummaryStatistics>(
                DoubleSummaryStatistics::new,
                (r, t) -> r.accept(mapper.applyAsDouble(t)),
                (l, r) -> { l.combine(r); return l; }, CH_ID);
    }
    

    并在自己的 class

    public class DoubleSummaryStatistics implements DoubleConsumer {
        private long count;
        private double sum;
        private double sumCompensation; // Low order bits of sum
        private double simpleSum; // Used to compute right sum for non-finite inputs
        private double min = Double.POSITIVE_INFINITY;
        private double max = Double.NEGATIVE_INFINITY;
    

相关问题