Stream.collect() 的花式玩法:Collector.of() 自定义收集器

📌 JDK 版本要求:JDK 8+(推荐 JDK 21)

一、为什么需要自定义收集器?

Java 提供了丰富的内置收集器:

java 复制代码
List<String> list = stream.collect(Collectors.toList());
Map<String, Integer> map = stream.collect(Collectors.toMap(k, v));
String joined = stream.collect(Collectors.joining(", "));

但当遇到以下场景,内置收集器就无能为力了:

  • 按固定大小分页(如每 3 个元素一组);
  • 收集到不可变集合(如 ImmutableList);
  • 同时计算多个统计量(如 min + max + count);
  • 将流转换为自定义对象(如 PageResult<T>)。

这时,Collector.of() 就派上用场了。

二、Collector 的四要素

一个 Collector<T, A, R> 由四个函数组成:

组件 类型 作用 关键约束
supplier () → A 创建累加器容器(Accumulator) 必须返回新实例(无状态)
accumulator (A, T) → void 将元素 T 合并到容器 A 必须是副作用操作(修改 A)
combiner (A, A) → A 合并两个容器(用于并行流) 必须满足结合律,且不修改输入
finisher (A) → R 将容器 A 转换为最终结果 R A == R 且不可变,可省略

⚠️ 重要概念区分

  • A(Intermediate Accumulator) :中间累加器,通常是可变容器 (如 ArrayList);
  • R(Final Result) :最终结果,可以是不可变对象 (如 StringImmutableList)。

三、基础示例:手写 toList()

我们先用 Collector.of() 重写 Collectors.toList(),以理解基本结构。

正确实现(可变容器 + 无 finisher)

java 复制代码
Collector<String, List<String>, List<String>> myToList = Collector.of(
    ArrayList::new,                 // supplier: 创建新列表
    List::add,                      // accumulator: 添加元素
    (left, right) -> {              // combiner: 合并两个列表
        left.addAll(right);
        return left;
    }
    // 无 finisher:因为 A (ArrayList) 可直接作为 R (List)
);

List<String> result = Stream.of("a", "b", "c")
    .collect(myToList);
// result = ["a", "b", "c"]

为什么没有 finisher?

因为累加器 ArrayList 本身就是合法的最终结果类型 List,且我们接受其可变性。

四、进阶示例:收集到不可变集合

若希望结果是不可变列表 (如 Guava 的 ImmutableList 或 JDK 21 的 List.copyOf()),就必须使用 finisher

正确实现(带 finisher)

java 复制代码
Collector<String, List<String>, List<String>> toImmutableList = Collector.of(
    ArrayList::new,
    List::add,
    (left, right) -> {
        left.addAll(right);
        return left;
    },
    Collections::unmodifiableList  // finisher: 包装为不可变视图
    // 或 JDK 10+: list -> List.copyOf(list)
);

🔍 关键点

  • 累加器 A 仍是可变的 ArrayList(高效添加);
  • 最终结果 R不可变的 List(通过 finisher 转换);
  • 不要 在 supplier 中直接返回 Collections.emptyList() ------ 它不可变,无法 add!

五、实战 1:按固定大小分页

需求 :将 [1,2,3,4,5,6,7] 按每页 3 个元素,收集为 [[1,2,3], [4,5,6], [7]]

正确实现

java 复制代码
public static <T> Collector<T, ?, List<List<T>>> toPages(int pageSize) {
    if (pageSize <= 0) throw new IllegalArgumentException("pageSize > 0");

    return Collector.of(
        () -> new ArrayList<List<T>>(), // A = List<List<T>>
        (pages, item) -> {
            if (pages.isEmpty() || pages.get(pages.size() - 1).size() >= pageSize) {
                pages.add(new ArrayList<>()); // 开新页
            }
            pages.get(pages.size() - 1).add(item); // 加入最后一页
        },
        (left, right) -> {
            if (left.isEmpty()) return right;
            if (right.isEmpty()) return left;

            // 合并:将 right 第一页追加到 left 最后一页(若未满)
            List<T> lastPage = left.get(left.size() - 1);
            List<T> firstPageOfRight = right.get(0);

            if (lastPage.size() < pageSize) {
                int space = pageSize - lastPage.size();
                int take = Math.min(space, firstPageOfRight.size());
                lastPage.addAll(firstPageOfRight.subList(0, take));
                if (take == firstPageOfRight.size()) {
                    left.addAll(right.subList(1, right.size()));
                } else {
                    List<T> remaining = new ArrayList<>(firstPageOfRight.subList(take, firstPageOfRight.size()));
                    List<List<T>> newRight = new ArrayList<>();
                    newRight.add(remaining);
                    newRight.addAll(right.subList(1, right.size()));
                    left.addAll(newRight);
                }
            } else {
                left.addAll(right);
            }
            return left;
        }
        // 无 finisher:A 已是所需 R
    );
}

使用

java 复制代码
List<Integer> input = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
List<List<Integer>> pages = input.stream().collect(toPages(3));
// pages = [[1,2,3], [4,5,6], [7]]

💡 注意 :combiner 逻辑复杂,因需处理"跨分区页合并"。若仅用于顺序流,可简化(但失去并行能力)。

六、实战 2:自定义字符串拼接(带前缀/后缀/分隔符)

虽然 Collectors.joining() 已很强大,但假设我们需要在拼接前对每个元素做特殊处理(如加引号)。

正确实现

java 复制代码
Collector<String, StringBuilder, String> quoteJoining = Collector.of(
    () -> new StringBuilder(),
    (sb, item) -> {
        if (sb.length() > 0) sb.append(", ");
        sb.append('"').append(item).append('"');
    },
    (left, right) -> left.append(", ").append(right), // 注意:right 已含内容
    StringBuilder::toString
);

String result = Stream.of("apple", "banana")
    .collect(quoteJoining);
// result = "\"apple\", \"banana\""

⚠️ 常见错误:在 combiner 中重复添加分隔符。

✅ 正确做法:combiner 应直接拼接两个完整片段,假设它们已正确格式化。

七、实战 3:同时计算 min、max、count(自定义统计对象)

需求:一次遍历,得到最小值、最大值、总数。

定义结果类

java 复制代码
public class Stats {
    private final int min;
    private final int max;
    private final long count;
    public Stats(int min, int max, long count) {
        this.min = min; this.max = max; this.count = count;
    }
    // getters...
}

实现收集器

java 复制代码
Collector<Integer, ?, Stats> statsCollector = Collector.of(
    () -> new int[]{Integer.MAX_VALUE, Integer.MIN_VALUE, 0}, // A = [min, max, count]
    (acc, value) -> {
        acc[0] = Math.min(acc[0], value);
        acc[1] = Math.max(acc[1], value);
        acc[2]++;
    },
    (a1, a2) -> {
        return new int[]{
            Math.min(a1[0], a2[0]),
            Math.max(a1[1], a2[1]),
            a1[2] + a2[2]
        };
    },
    acc -> new Stats(acc[0], acc[1], acc[2]) // finisher
);

优势:单次遍历,并行安全,内存高效。

八、Collector.of() vs Collectors.reducing()

场景 推荐方案
简单归约(如 sum、max) Collectors.reducing()
需要可变中间容器(如 list、map) Collector.of()
需要自定义结果类型 Collector.of()
并行流 + 复杂合并逻辑 Collector.of()(显式控制 combiner)

💡 reducing 本质是 immutable fold,而 Collector.of 更适合 mutable accumulation。

九、Spring Boot / 生产环境建议

  1. 优先使用内置收集器toList(), toUnmodifiableList(), groupingBy() 等已高度优化;
  2. 自定义收集器务必单元测试:覆盖顺序/并行、空流、单元素等边界;
  3. 避免在 Web 层滥用复杂收集器:可读性 > 性能微优化;
  4. 考虑使用第三方库 :如 Guava 的 ImmutableList.toImmutableList()

十、代码在哪?

本篇涉及到的代码已上传至 GitHub:

https://github.com/iweidujiang/java-tricks-lab

欢迎 star & fork !

相关推荐
丶小鱼丶2 小时前
数据结构和算法之【队列】
java·数据结构
菜鸡儿齐5 小时前
Unsafe方法学习
java·python·学习
汤姆yu5 小时前
IDEA接入Claude Code保姆级教程(Windows专属+衔接前置安装)
java·windows·intellij-idea·openclaw·openclasw安装
prince058 小时前
用户积分系统怎么设计
java·大数据·数据库
967710 小时前
理解IOC控制反转和spring容器,@Autowired的参数的作用
java·sql·spring
SY_FC10 小时前
实现一个父组件引入了子组件,跳转到其他页面,其他页面返回回来重新加载子组件函数
java·前端·javascript
耀耀_很无聊10 小时前
09_Jenkins安装JDK环境
java·运维·jenkins
ノBye~10 小时前
Centos7.6 Docker安装redis(带密码 + 持久化)
java·redis·docker
黑臂麒麟10 小时前
openYuanrong:多语言运行时独立部署以库集成简化 Serverless 架构 & 拓扑感知调度:提升函数运行时性能
java·架构·serverless·openyuanrong