正确的 Java Streams 分块方式 — 一个在 JDK 中应有的 Collector

发布: (2025年12月11日 GMT+8 23:46)
5 min read
原文: Dev.to

Source: Dev.to

Chunking Java Streams 的正确做法 — 一个应该出现在 JDK 中的 Collector 的封面图

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

分块是一个普遍问题,现在终于有了一个干净、可复用、友好的流式解决方案。

如果你曾经想过,“为什么没有直接内置的办法?”——现在有了。

试一试,给仓库加星,留下反馈——我很想了解你是怎么使用它的。

Back to Blog

相关文章

阅读更多 »

Java-8(最大的转变)

Lambda Expressions Part 1 在 Java 中,即使是编写一小段逻辑也曾经很冗长,因为我们必须创建一个匿名类并覆盖其方法 e...

Java 8(Stream API)

Stream API 的特性 - 声明式 – 使用函数式风格编写简洁且可读的代码。 - 惰性求值 – 只有在终端操作时才会执行操作……