如何高效地扁平化 List:避免内存复制的最优实践

:避免内存复制的最优实践 ">:避免内存复制的最优实践 " />

本文介绍在 java 中将 `list`(每个对象包含 `list`)扁平化为单层 `list` 的最高效方法,重点通过预分配容量消除 arraylist 动态扩容带来的多次数组复制开销。

在高吞吐、低延迟的服务场景中,对嵌套列表(如 List,其中每个 Object 持有 List childList)执行扁平化操作,常因不当的集合操作成为 CPU 瓶颈。你已尝试的两种方式——Stream.flatMap().collect() 和 forEach + addAll()——虽语义清晰,但存在共性性能缺陷:未预估目标集合容量,导致 ArrayList 在内部反复扩容、复制底层数组,时间复杂度退化为 O(n²)(平均而言),尤其当总元素量达数万以上时尤为明显。

最优解:预计算总大小 + 批量添加

核心思想是用一次遍历估算最终容量,再用一次遍历填充数据,全程规避扩容:

// 第一步:精确计算所有 childList 的总元素数(O(n) 时间,假设 size() 是 O(1))
int expectedSize = parentList.stream()
    .mapToInt(obj -> obj.getChildList().size())
    .sum();

// 第二步:初始化具有精确容量的 ArrayList(避免任何内部扩容)
List result = new ArrayList<>(expectedSize);

// 第三步:逐个添加子列表(addAll 内部使用 System.arraycopy,高效且无扩容)
for (Object obj : parentList) {
    result.addAll(obj.getChildList());
}

? 为什么这更高效?

  • ArrayList(int initialCapacity) 构造器直接分配足够空间,后续 addAll() 仅执行内存块拷贝(System.arraycopy),时间复杂度为 O(k),k 为待添加元素数;
  • 相比之下,未指定容量的 new ArrayList() 默认容量为 10,若总元素为 100,000,则需约 17 次扩容(每次扩容约 1.5 倍),累计复制元素超 200,000 次;
  • Stream.flatMap 虽函数式优雅,但 Collectors.toList() 返回的 ArrayList 无法预设容量(JDK 当前实现不支持),且流式处理本身有额外装箱/迭代器开销。

⚠️ 注意事项与前提条件

  • ✅ 前提:childList.size() 必须是 O(1) 操作(如 ArrayList、LinkedList 均满足;若为自定义慢速 size() 实现则不适用);
  • ✅ 推荐:若 parentList 本身很大(如 > 10,000),可将 stream().mapToInt(...).sum() 替换为传统 for 循环,避免 Stream 创建开销;
  • ⚠️ 避免:不要使用 Collections.addAll(result, ...),它不接受 Collection 参数,且无法批量插入 List;
  • ? 扩展:若需不可变结果,可在构建完成后调用 Collections.unmodifiableList(result),但切勿在构建过程中包装——会破坏性能。

总结:预分配容量

是 Java 集合扁平化的黄金准则。相比函数式写法,显式控制容量的双遍历方案在真实业务负载下通常可降低 30%~70% 的 CPU 占用,尤其适用于服务端高频调用路径。务必在性能敏感代码中优先考虑容量预估,而非依赖“自动”集合行为。