正确的 Java Streams 分块方式 — 一个在 JDK 中应有的 Collector
Source: Dev.to

Chunking Java Streams the Right Way — Finally, a Collector That Feels Like It Should Be in the JDK
如果你曾经需要把一个大列表或流拆分成大小相等的块,你一定体会过这种痛苦:
- 写一个循环。
- 更糟的是,写嵌套循环。
- 可能需要一个计数器。
- 可能需要一个临时列表。
- 可能还有一些几乎能工作,直到某个边缘情况把它炸掉的代码。
对元素进行分块是那种日常操作,却始终没有进入 JDK。开发者在每个项目里都重复编写同样的工具方法……每次又稍有不同。
在遇到 PostgreSQL 驱动的限制(迫使我把成千上万的 UUID 批量拆成更小的块)后,我决定:
这应该是一个 Collector。 简洁、可组合、为 Streams 而生。
于是我实现了它。
这就是 Chunking Collector —— 一个轻量级的 Java 8+ 库,让你以一种读起来像标准库的方式表达分块。
🔥 The Old Way: Manual Chunking (A Bit of a Mess)
List<List<T>> chunks = new ArrayList<>();
List<T> current = new ArrayList<>();
for (T item : items) {
current.add(item);
if (current.size() == chunkSize) {
chunks.add(current);
current = new ArrayList<>();
}
}
if (!current.isEmpty()) {
chunks.add(current);
}
它能工作……但并不总是:
- 难以阅读
- 容易出错
- 不能复用
- 不友好并行化
- 不友好流式操作
而且它打断了代码的自然流——本该用 Stream 管道表达的逻辑被迫写成了循环。
✨ The New Way: A Collector That Just Works
使用 Chunking Collector,你只需这样写:
List<List<Integer>> chunks = numbers.stream()
.collect(Chunking.toChunks(3));
输出
[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
可读、可靠、可预测。这才是分块应该有的感觉。
🧩 Why a Collector?
分块本质上是一个归约操作:
- 输入一个 Stream
- 输出一个 List of Lists
- 没有副作用
- 不会泄漏可变状态
- 能自然地与有序、并行或顺序流配合
更重要的是,这完美契合了 Stream 的哲学:
stream.collect(Chunking.toChunks(size));
一眼就能看出它的作用。
📦 Installation
<dependency>
<groupId>dev.zachmaddox</groupId>
<artifactId>chunking-collector</artifactId>
<version>1.1.0</version>
</dependency>
或在 Gradle 中使用:
implementation 'dev.zachmaddox:chunking-collector:1.1.0'
🧠 Practical Examples That Come Up All the Time
1. Batch Processing
Chunking.chunk(records, 100)
.forEach(batch -> processBatch(batch));
2. Database Paging
var pages = results.stream()
.collect(Chunking.toChunks(500));
3. Parallel Workloads
Chunking.chunk(items, 10)
.parallelStream()
.forEach(this::processChunk);
🔥 The Real Origin: Working Around PostgreSQL IN‑Clause Limits
PostgreSQL(以及许多 JDBC 驱动)限制单条 SQL 语句中参数列表的长度。分块能够干净且安全地使用参数化 SQL 解决此问题:
NamedParameterJdbcTemplate named = new NamedParameterJdbcTemplate(jdbcTemplate);
Chunking.chunk(ids, 500)
.parallelStream()
.map(chunk -> named.query(
"SELECT * FROM users WHERE id IN (:ids)",
Map.of("ids", chunk),
(rs, n) -> mapRow(rs)
))
.flatMap(List::stream)
.toList();
结果
- 没有驱动错误
- 更小、更快的查询
- 代码清晰、易维护
- 可并行化的工作负载
仅此一点就足以让我决定实现这个库。
⚡ Advanced Capabilities (When You Need Them)
Chunking Collector 已经发展成一个灵活的工具箱:
- 余数策略(
INCLUDE_PARTIAL,DROP_PARTIAL) - 自定义 List 工厂
- 惰性块流
- 滑动窗口
- 基于边界的分块
- 加权分块
- 原始类型流助手
核心 API 仍保持极其简洁。
🧩 Design Philosophy
“如果这个 API 有一天真的进入 JDK,没人会感到惊讶。”
- 零依赖
- 零反射
- 零魔法
- 纯净的 Java
- 极小的表面面积
- 完全符合有经验的 Java 开发者的预期
📚 Full Documentation
- JavaDoc:
- GitHub:
- Maven Central:
🎉 Final Thoughts
分块是一个普遍问题,现在终于有了一个干净、可复用、友好的流式解决方案。
如果你曾经想过,“为什么没有直接内置的办法?”——现在有了。
试一试,给仓库加星,留下反馈——我很想了解你是怎么使用它的。