本文介绍如何使用 Java 8 Stream 和 Collectors 高效地将 FileInfo 对象列表按 fileSize 累计值动态分组,确保每组总大小不超过指定阈值,并返回 List 结构。
本文介绍如何使用 java 8 stream 和 collectors 高效地将 fileinfo 对象列表按 filesize 累计值动态分组,确保每组总大小不超过指定阈值,并返回 `list>` 结构。
在批量处理文件元数据(如上传分片、归档打包或资源调度)时,常需将一组 FileInfo 按累计大小“装箱”——即划分成若干子列表,使每个子列表中所有文件的 fileSize 总和 ≤ 给定上限(例如 200 MB)。虽然传统 for 循环易于理解,但借助 Stream API 可写出更函数式、可组合的实现。关键挑战在于:分组逻辑依赖前序元素的累计状态,而标准 Collectors.groupingBy 是无状态的,因此需巧妙结合 AtomicLong 与 collectingAndThen 实现伪状态累积。
核心思路是:为每个 FileInfo 动态计算一个“分组键”,该键由当前累计 fileSize 除以阈值后向上取整得到。由于 groupingBy 的分类函数(Function
private Function<FileInfo, String> fileInfoFunction() {
final AtomicLong fileSizeAccumulator = new AtomicLong(0L);
return file -> String.valueOf(
Math.ceil((double) fileSizeAccumulator.addAndGet(file.getFileSize()) / FILE_GROUP_LIMIT)
);
}注意:此方法隐含顺序敏感性——必须保证流为有序串行流(默认 stream() 满足),且不可并行(parallelStream() 会破坏累计逻辑)。完整处理流程如下:
- 使用 Collectors.groupingBy(keyFunc, toList()) 将元素映射到 Map
>; - 通过 Collectors.collectingAndThen 在 grouping 后立即转换:提取 Map.entrySet() 流,map(entry -> entry.getValue()) 获取各组列表,再收集为 List
- >。
以下是精简可运行的示例代码:
分析 Java Spring Boot 后端 Controller 代码,自动生成 Markdown 格式的接口文档。当用户说“生成 Java 接口文档”、“生成 API 文档”、“从 Spring 代码生成文档”时触发。
立即学习“Java免费学习笔记(深入)”;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
public class FileInfoGrouper {
private static final double FILE_GROUP_LIMIT = 200.0;
public static List<List<FileInfo>> groupByCumulativeSize(List<FileInfo> files) {
AtomicLong sum = new AtomicLong(0L);
return files.stream()
.collect(Collectors.collectingAndThen(
Collectors.groupingBy(
f -> String.valueOf(
Math.ceil((double) sum.addAndGet(f.getFileSize()) / FILE_GROUP_LIMIT)
),
Collectors.toList()
),
map -> new ArrayList<>(map.values())
));
}
// 示例用法
public static void main(String[] args) {
List<FileInfo> files = Arrays.asList(
new FileInfo("a.txt", 120L, "OK"),
new FileInfo("b.txt", 95L, "OK"),
new FileInfo("c.txt", 80L, "OK"),
new FileInfo("d.txt", 150L, "OK")
);
List<List<FileInfo>> groups = groupByCumulativeSize(files);
groups.forEach(group -> {
long total = group.stream().mapToLong(FileInfo::getFileSize).sum();
System.out.printf("Group (size=%d): %s%n", total, group);
});
// 输出:
// Group (size=120): [FileInfo{filePath='a.txt', fileSize=120, status='OK'}]
// Group (size=175): [FileInfo{filePath='b.txt', fileSize=95, status='OK'},
// FileInfo{filePath='c.txt', fileSize=80, status='OK'}]
// Group (size=150): [FileInfo{filePath='d.txt', fileSize=150, status='OK'}]
}
}⚠️ 重要注意事项:
- 线程不安全:AtomicLong 仅保障单次 addAndGet 原子性,但整个分组逻辑不可用于并行流,否则累计顺序错乱;
- 边界敏感:若某单个文件 fileSize > FILE_GROUP_LIMIT,它将独占一组(因 ceil(x) = x 当 x > 1),符合预期;
- 键稳定性:使用 String.valueOf(ceil(...)) 作为键,避免浮点精度问题;也可改用 Long 键(如 (sum.get() - 1) / limit + 1)提升性能;
- 替代方案提醒:对复杂状态聚合(如带重置的滑动窗口),建议使用 Stream.reduce() 或自定义 Collector,而非强行适配 groupingBy。
总结:本方案以简洁的 Stream 链式调用实现了带状态的累计分组,在保持代码可读性的同时,充分利用了 Collector 的组合能力。适用于中小规模、顺序敏感的批处理场景。

















