欢迎光临
我们一直在努力

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

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

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

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):最终结果,可以是不可变对象(如 String、ImmutableList)。

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

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

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

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)

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]]。

正确实现

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
);
}

使用

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() 已很强大,但假设我们需要在拼接前对每个元素做特殊处理(如加引号)。

正确实现

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(自定义统计对象)

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

定义结果类

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…
}

实现收集器

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 / 生产环境建议

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

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

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

    欢迎 star & fork !

    赞(0)
    未经允许不得转载:171主机测评 » Stream.collect() 的花式玩法:Collector.of() 自定义收集器
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址