0Pricing
Java Academy · 课时

构建自定义收集器

实现 Collector 接口,为特定领域的使用场景创建可复用的聚合器

构建自定义收集器 是 CoddyKit 上的免费 Java Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Java Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Java Academy 课程共包含 4 节课。

为什么要构建自定义收集器?

当内置收集器无法满足您的业务领域需求时,可以实现 Collector<T, A, R> 接口,创建可复用、可组合的聚合逻辑。T=输入,A=可变累加器,R=结果。

收集器接口

收集器包含五个组成部分:

  • supplier()——创建初始累加器
  • accumulator()——将一个元素折叠到累加器中
  • combiner()——合并两个累加器(用于并行流)
  • finisher()——将累加器转换为最终结果
  • characteristics()——提供优化提示

Collector.of() 工厂方法

创建自定义收集器最简单的方式,是使用 Collector.of(),无需直接实现接口:

import java.util.stream.*;

// Collector that builds a comma-separated String
Collector<String, StringBuilder, String> csvCollector = Collector.of(
    StringBuilder::new,                     // supplier
    (sb, s) -> {
        if (sb.length() > 0) sb.append(',');
        sb.append(s);
    },                                       // accumulator
    (a, b) -> {
        if (a.length() > 0 && b.length() > 0) a.append(',');
        a.append(b); return a;
    },                                       // combiner
    StringBuilder::toString                  // finisher
);

String csv = Stream.of("Alice","Bob","Carol").collect(csvCollector);
System.out.println(csv); // Alice,Bob,Carol

自定义收集器:前 N 项

构建一个仅按比较器保留前 N 个元素的收集器:

import java.util.*;
import java.util.stream.*;

static <T> Collector<T, ?, List<T>> topN(int n, Comparator<T> comp) {
    return Collector.of(
        () -> new ArrayList<T>(),
        (list, e) -> {
            list.add(e);
            list.sort(comp.reversed());
            if (list.size() > n) list.remove(list.size() - 1);
        },
        (a, b) -> {
            a.addAll(b);
            a.sort(comp.reversed());
            while (a.size() > n) a.remove(a.size() - 1);
            return a;
        },
        Collector.Characteristics.UNORDERED
    );
}

使用前 N 项收集器

在流上应用自定义的前 N 项收集器:

List<Integer> top3 = Stream.of(5,2,9,1,7,3,8)
    .collect(topN(3, Comparator.naturalOrder()));
System.out.println(top3); // [9, 8, 7]

特性标志

三个可选特性可用于调整流的优化:

  • IDENTITY_FINISH——完成器是恒等变换;跳过完成器
  • UNORDERED——结果顺序无关紧要
  • CONCURRENT——可以并发调用累加器

设置错误的标志(例如在不具备线程安全性的情况下设置 CONCURRENT)会导致错误。

自定义收集器:运行平均值

无需进行第二次遍历即可维护运行平均值:

record RunningAvg(long count, double sum) {
    RunningAvg add(double v) { return new RunningAvg(count+1, sum+v); }
    RunningAvg merge(RunningAvg o) { return new RunningAvg(count+o.count, sum+o.sum); }
    double avg() { return count == 0 ? 0 : sum / count; }
}

Collector<Double, RunningAvg[], Double> avgCollector = Collector.of(
    () -> new RunningAvg[]{new RunningAvg(0, 0)},
    (a, v) -> a[0] = a[0].add(v),
    (a, b) -> { a[0] = a[0].merge(b[0]); return a; },
    a -> a[0].avg()
);

double avg = Stream.of(10.0, 20.0, 30.0).collect(avgCollector);
System.out.println(avg); // 20.0

直接实现收集器接口

如需更多控制,可以将 Collector<T,A,R> 实现为一个类:

import java.util.function.*;
import java.util.stream.*;

class StringJoiningCollector implements Collector<String, List<String>, String> {
    private final String delimiter;
    StringJoiningCollector(String delimiter) { this.delimiter = delimiter; }
    public Supplier<List<String>> supplier() { return ArrayList::new; }
    public BiConsumer<List<String>, String> accumulator() { return List::add; }
    public BinaryOperator<List<String>> combiner() {
        return (a,b) -> { a.addAll(b); return a; };
    }
    public Function<List<String>, String> finisher() {
        return list -> String.join(delimiter, list);
    }
    public Set<Characteristics> characteristics() { return Set.of(); }
}

自定义收集器与内置收集器的比较

在编写自定义收集器之前,请先检查组合内置收集器是否可以达到相同结果——内置收集器已经过测试和优化:

// Instead of custom, compose:
Map<String, IntSummaryStatistics> stats = orders.stream().collect(
    Collectors.groupingBy(Order::status,
        Collectors.summarizingInt(Order::amount))
);
// This replaces many custom collector implementations

自定义收集器中的线程安全

如果设置了 CONCURRENT 特性,多个线程会同时调用累加器。因此,累加器必须是线程安全的(例如使用 ConcurrentHashMap 或 AtomicLong)。如果没有设置 CONCURRENT,则由组合器安全地处理并行操作。

实际应用:直方图收集器

将数值分桶到直方图映射中:

Collector<Integer, Map<Integer, Long>, Map<Integer, Long>> histogram =
    Collector.of(
        LinkedHashMap::new,
        (map, val) -> map.merge(val / 10 * 10, 1L, Long::sum),
        (a, b) -> { b.forEach((k,v) -> a.merge(k,v,Long::sum)); return a; },
        Collector.Characteristics.IDENTITY_FINISH
    );

Map<Integer,Long> hist = Stream.of(5,15,25,12,8,22,18,3)
    .collect(histogram);
hist.forEach((bucket, cnt) -> System.out.println(bucket+"-"+(bucket+9)+": "+cnt));

快速检查

哪个收集器特性标志表示完成函数是恒等变换,因此可以跳过?

回顾:自定义收集器

要点回顾:

  • 收集器<T,A,R>:T=输入,A=累加器,R=结果
  • 使用 Collector.of(),无需创建类即可快速实现
  • 供应器、累加器、组合器、完成器和特性是五个组成部分
  • 身份完成、UNORDERED、CONCURRENT 是优化标志
  • 优先组合内置收集器;只有在必要时才自定义

常见问题解答

「构建自定义收集器」课时是免费的吗?

是的 — 「构建自定义收集器」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Java Academy 课程的其余内容,请升级到 CoddyKit PRO。 Java Academy 课程共包含 4 节课。

「构建自定义收集器」这节课中我会学到什么?

实现 Collector 接口,为特定领域的使用场景创建可复用的聚合器 你通过在浏览器中直接运行的动手代码来练习 Java Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Java Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Java Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「构建自定义收集器」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Java Academy 课中编写并运行代码吗?

能。每节 Java Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. groupingBy:元素分类
  2. partitioningBy 与计数
  3. toMap、joining 与 summarizing
  4. 构建自定义收集器
← 返回 Java Academy