【问题标题】:How to use a custom Collector in a groupingBy operation如何在 groupingBy 操作中使用自定义收集器
【发布时间】:2018-09-27 10:05:51
【问题描述】:

带有流的Oracle trails on reduction 提供了一个示例,说明如何将一组人转换为包含基于性别的平均年龄的地图。它使用以下Person 类和代码:

public class Person {
    private int age;

    public enum Sex {
        MALE,
        FEMALE
    }

    private Sex sex;

    public Person (int age, Sex sex) {
        this.age = age;
        this.sex = sex;
    }

    public int getAge() { return this.age; }

    public Sex getSex() { return this.sex; }
}

Map<Person.Sex, Double> averageAgeByGender = roster
    .stream()
    .collect(
        Collectors.groupingBy(
            Person::getSex,                      
            Collectors.averagingInt(Person::getAge)));

上面的流代码效果很好,但我想看看如何在使用收集器的自定义实现时执行相同的操作。我在 Stack Overflow 或网络上都找不到完整的示例。至于我们为什么要这样做,例如,也许我们想要计算某种涉及年龄的加权平均值。在这种情况下,Collectors.averagingInt 的默认行为是不够的。

【问题讨论】:

  • 不确定在这种情况下是否有帮助,但也有Collectors.summarizingInt,它返回一个IntSummaryStatistics,当Collectors.averagingInt 不够用时很有帮助。
  • @kapex 这很有帮助,但我的目的是为罐装收集器满足需求的情况提供一些可用的样板代码。

标签: java lambda java-stream collectors


【解决方案1】:

在这些情况下只需使用Collector.of(Supplier, BiConsumer, BinaryOperator, [Function,] Characteristics...)

Collector.of(() -> new double[2],
        (a, t) -> { a[0] += t.getAge(); a[1]++; },
        (a, b) -> { a[0] += b[0]; a[1] += b[1]; return a; },
        a -> (a[1] == 0) ? 0.0 : a[0] / a[1])
)

虽然定义PersonAverager 可能更具可读性:

class PersonAverager {
    double sum = 0;
    int count = 0;

    void accept(Person p) {
        sum += p.getAge();
        count++;
    }

    PersonAverager combine(PersonAverager other) {
        sum += other.sum;
        count += other.count;
        return this;
    }

    double average() {
        return count == 0 ? 0 : sum / count;
    }
}

并将其用作:

Collector.of(PersonAverager::new,
        PersonAverager::accept,
        PersonAverager::combine,
        PersonAverager::average)

【讨论】:

  • @TimBiegeleisen 你现在可以接受了,这些是创建自定义Collector...的唯一方法...是的,绝对是 1+
  • 我正在写一个包含这个确切内容的答案... +1 一个改进是使用一个命名良好的实用程序方法,在该方法中将 PersonAverager 定义为本地类,然后返回使用Collector.of 创建的收集器。这样,您将得到非常易读的代码:Map&lt;Person.Sex, Double&gt; averageAgeBySex = list.stream().collect(Collectors.groupingBy(Person::getSex, averagingInt(Person::getAge)));,其中averagingInt 是静态实用程序方法的名称。
  • @FedericoPeraltaSchaffner 老实说,我想这样做,但你已经有Collectors.averingInt/Long/Double() 可以做到这一点(对于double 来说更好)。我认为在这里保持简单会更重要,因为当您想做除平均之外的其他事情时,它更容易概括。
  • 我同意,这就是你的答案。此外,使用静态方法是个人喜好问题,不值得有新的答案。对于这种情况(averagingX),既不使用自定义收集器也不使用静态方法没有意义,但对于更复杂的情况,它可能会得到回报。
【解决方案2】:

这个答案已经过测试,是基于一堆不同的来源。 Collectors#averagingInt 的源代码有助于弄清楚下面使用的 lambda 语法。使用的供应商是一个大小为 2 的 Double[] 数组。第一个索引用于存储人的累计年龄,而第二个索引用于存储计数。

public class PersonCollector<T extends Person> implements Collector<T, double[], Double> {
    private ToIntFunction<Person> mapper;

    public PersonCollector(ToIntFunction<Person> mapper) {
        this.mapper = mapper;
    }

    @Override
    public Supplier<double[]> supplier() {
        return () -> new double[2];
    }

    @Override
    public BiConsumer<double[], T> accumulator() {
        return (a, t) -> { a[0] += mapper.applyAsInt(t); a[1]++; };
    }

    @Override
    public BinaryOperator<double[]> combiner() {
        return (a, b) -> { a[0] += b[0]; a[1] += b[1]; return a; };
    }

    @Override
    public Function<double[], Double> finisher() {
        return a -> (a[1] == 0) ? 0.0 : a[0] / a[1];
    }

    @Override
    public Set<Characteristics> characteristics() {
        // do NOT return IDENTITY_FINISH here, which would bypass
        // the custom finisher() above
        return Collections.emptySet();
    }
}

List<Person> list = new ArrayList<>();
list.add(new Person(34, Person.Sex.MALE));
list.add(new Person(23, Person.Sex.MALE));
list.add(new Person(68, Person.Sex.MALE));
list.add(new Person(14, Person.Sex.FEMALE));
list.add(new Person(58, Person.Sex.FEMALE));
list.add(new Person(27, Person.Sex.FEMALE));

final Collector<Person, double[], Double> pc = new PersonCollector<>(Person::getAge);

Map<Person.Sex, Double> averageAgeBySex = list
  .stream()
  .collect(Collectors.groupingBy(Person::getSex, pc));

System.out.println("Male average: " + averageAgeBySex.get(Person.Sex.MALE));
System.out.println("Female average: " + averageAgeBySex.get(Person.Sex.FEMALE));

这个输出:

Male average: 41.666666666666664
Female average: 33.0

请注意,我们将方法引用Person::getAge 传递给自定义收集器,它将集合中的每个Person 映射到一个整数年龄值。此外,我们不会从 characateristics() 方法返回 Characteristics.IDENTITY_FINISH。这样做意味着我们的自定义 finisher() 将被绕过。

【讨论】:

  • 也可以使用Collector.of(...)工厂方法创建自定义收集器。
  • 好吧,Collector.of(() -&gt; new double[2], (a, t) -&gt; { a[0] += mapper.applyAsInt(t); a[1]++; }, (a, b) -&gt; { a[0] += b[0]; a[1] += b[1]; return a; }, a -&gt; a[1] == 0? 0.0: a[0] / a[1]) 就可以了。
  • 我欢迎其他答案。如果您可以评论为什么使用完全内联的 Collector.of 版本比我使用的更详细的版本更合适,那很好。
  • @TimBiegeleisen IMO much 更容易阅读,而且我们阅读代码 比我们编写的更多...
  • @Eugene 够公平的。不过老实说,我发现阅读内联版本很难,因为我不知道组件的顺序。但是,我敢肯定,一旦熟悉了 API,就会有优势。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多