本文介绍如何使用 Java 8 Stream 和 Collectors 将 FileInfo 列表按 fileSize 累计值动态分组为多个子列表,确保每组总大小不超过指定阈值,并最终返回 List<List> 结构。
本文介绍如何使用 java 8 stream 和 collectors 将 fileinfo 列表按 filesize 累计值动态分组为多个子列表,确保每组总大小不超过指定阈值,并最终返回 list> 结构。
在实际文件批处理、上传分片或资源调度场景中,常需将一组文件对象(如 FileInfo)按总大小“装箱”——即划分为若干逻辑组,使每组内文件大小之和 ≤ 给定上限(如 200 MB)。虽然传统 for 循环易于理解,但使用 Stream API 可提升代码声明性与函数式表达力。需要注意的是:标准 Collectors 不直接支持基于运行累计值的分组(stateful grouping),因此需谨慎结合 AtomicLong 实现线程安全的累积逻辑,并通过 groupingBy 的分组键动态计算实现目标效果。
核心思路是:为每个 FileInfo 计算一个“组号”(group ID),该 ID 由当前累计 fileSize 除以阈值后向上取整得到。由于分组键需在流遍历过程中递增生成,我们借助 AtomicLong 维护全局累计值,并在 Function<FileInfo, String> 中实时更新并返回组标识。
以下是完整可运行示例:
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.stream.Collectors;
public class FileInfoProcessor {
private static final double FILE_GROUP_LIMIT = 200.0;
public static void main(String[] args) {
List<FileInfo> files = List.of(
new FileInfo("A", 100L, "Active"),
new FileInfo("B", 100L, "Active"),
new FileInfo("C", 100L, "Active"),
new FileInfo("D", 100L, "Active")
);
List<List<FileInfo>> grouped = processFiles(files);
grouped.forEach(System.out::println);
// 输出:
// [FileInfo{filePath='A', fileSize=100, status='Active'}, FileInfo{filePath='B', fileSize=100, status='Active'}]
// [FileInfo{filePath='C', fileSize=100, status='Active'}, FileInfo{filePath='D', fileSize=100, status='Active'}]
}
public static List<List<FileInfo>> processFiles(List<FileInfo> files) {
AtomicLong accumulator = new AtomicLong(0L);
return files.stream()
.collect(Collectors.collectingAndThen(
Collectors.groupingBy(
fileInfo -> String.valueOf(
(long) Math.ceil((double) accumulator.addAndGet(fileInfo.fileSize) / FILE_GROUP_LIMIT)
),
LinkedHashMap::new, // 保持插入顺序(可选但推荐)
Collectors.toList()
),
map -> new ArrayList<>(map.values())
));
}
public static class FileInfo {
private final String filePath;
private final Long fileSize;
private final String status;
public FileInfo(String filePath, Long fileSize, String status) {
this.filePath = filePath;
this.fileSize = fileSize;
this.status = status;
}
// getter methods omitted for brevity (required for compilation)
public String getFilePath() { return filePath; }
public Long getFileSize() { return fileSize; }
public String getStatus() { return status; }
@Override
public String toString() {
return "FileInfo{" +
"filePath='" + filePath + '\'' +
", fileSize=" + fileSize +
", status='" + status + '\'' +
'}';
}
}
}✅ 关键要点说明:
立即学习“Java免费学习笔记(深入)”;
- 使用 AtomicLong 确保多线程环境下的累计安全(即使当前流为单线程,也符合函数式编程的无副作用预期);
- 分组键为 String.valueOf(ceil(累计值 / 限值)),天然实现“装满即换组”的语义;
- LinkedHashMap::new 保证分组结果按原始顺序排列,避免因哈希随机性导致组内顺序混乱;
- collectingAndThen 链式转换 Map<K, List<V>> → Collection<List<V>> → List<List<V>>,简洁完成结构重塑。
⚠️ 注意事项:
- 此方案不适用于并行流(parallelStream()),因为 AtomicLong 累加依赖严格顺序,而并行流无法保证元素处理顺序,将导致分组逻辑失效;
- 若需更高鲁棒性(如空列表、null fileSize),应在 processFiles 中添加前置校验;
- 对于超大列表,建议配合 Collectors.toCollection(ArrayList::new) 控制中间集合类型,避免默认 ArrayList 容量频繁扩容。
综上,该方案以清晰的函数式链式调用,实现了基于运行时累计值的动态分组需求,在保持 Stream 风格的同时兼顾可读性与实用性。


















