优化 java arraylist 批量操作性能的核心在于减少动态扩容带来的数组拷贝开销,并在遍历或修改时避免并发修改异常。最直接的做法是在初始化时通过构造函数指定预估容量,从而消除 resize 过程中的内存分配与 system.arraycopy 调用;若涉及大量添加,可预先计算所需大小并一次性扩容。对于读取密集型场景,使用增强 for 循环(迭代器)或传统索引循环比 stream api 更高效,因为后者会引入对象创建和中间状态管理的额外开销。

ArrayList 初始化时的容量预分配策略
在构建 ArrayList 时直接传入预估的初始容量,可以彻底避免默认容量下的多次扩容。Java 的 ArrayList 底层基于数组实现,当元素数量超过当前容量时,会自动创建一个新数组并将旧数据拷贝过去,这个过程在批量插入大量数据时非常耗时。通过 new ArrayList(expectedSize) 指定大小,JVM 只需分配一次内存空间,后续 add 操作仅涉及简单的赋值,无需触发 grow() 方法中的扩容逻辑。

import java.util.ArrayList;
import java.util.List;
public class CapacityOptimization {
public static void main(String[] args) {
// 假设已知需要存储 10000 个元素
int expectedSize = 10000;
// 错误做法:使用无参构造函数,默认容量为 10,会频繁扩容
List<String> badList = new ArrayList<>();
long startBad = System.nanoTime();
for (int i = 0; i < expectedSize; i++) {
badList.add("item-" + i);
}
long endBad = System.nanoTime();
System.out.println("Default capacity time: " + (endBad - startBad) / 1_000_000.0 + " ms");
// 正确做法:指定初始容量,避免扩容
List<String> goodList = new ArrayList<>(expectedSize);
long startGood = System.nanoTime();
for (int i = 0; i < expectedSize; i++) {
goodList.add("item-" + i);
}
long endGood = System.nanoTime();
System.out.println("Pre-sized capacity time: " + (endGood - startGood) / 1_000_000.0 + " ms");
}
}
运行上述代码,badList 会因为默认的 10、15、22... 等扩容序列产生多次数组拷贝,而 goodList 仅分配一次内存。预期结果是 Pre-sized capacity time 显著小于 Default capacity time,尤其是在数据量较大时差异明显。注意,如果预估容量远大于实际使用量,虽然避免了扩容,但会浪费堆内存,因此预估应尽量准确。
立即学习“Java免费学习笔记(深入)”;
批量遍历与 Stream API 的性能对比
在处理 ArrayList 的批量读取或简单转换时,传统的索引循环或增强 for 循环通常比 Java 8 引入的 Stream API 更快。Stream 提供了函数式编程的便利,但其内部涉及迭代器的创建、中间操作的链式调用以及可能的装箱/拆箱开销。对于纯 CPU 密集型的遍历任务,直接操作底层数组或使用索引访问能减少这些隐式成本。

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class TraversalPerformance {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>(100000);
for (int i = 0; i < 100000; i++) {
list.add(i);
}
// 方式一:传统索引循环
long startIdx = System.nanoTime();
int sumIdx = 0;
for (int i = 0; i < list.size(); i++) {
sumIdx += list.get(i);
}
long endIdx = System.nanoTime();
System.out.println("Index loop sum: " + sumIdx + ", time: " + (endIdx - startIdx) / 1_000_000.0 + " ms");
// 方式二:增强 for 循环
long startFor = System.nanoTime();
int sumFor = 0;
for (Integer val : list) {
sumFor += val;
}
long endFor = System.nanoTime();
System.out.println("Enhanced for sum: " + sumFor + ", time: " + (endFor - startFor) / 1_000_000.0 + " ms");
// 方式三:Stream API
long startStream = System.nanoTime();
int sumStream = list.stream().mapToInt(Integer::intValue).sum();
long endStream = System.nanoTime();
System.out.println("Stream sum: " + sumStream + ", time: " + (endStream - startStream) / 1_000_000.0 + " ms");
}
}
在上述示例中,list.stream().mapToInt(...) 会创建流管道,尽管 mapToInt 减少了装箱开销,但仍需处理流的状态管理。预期输出显示,Index loop 和 Enhanced for 的执行时间通常低于 Stream,特别是在数据量达到十万级别时。如果业务逻辑复杂,必须使用 Stream,可以考虑并行流 parallelStream(),但这会增加线程调度开销,仅在多核且数据量极大时才可能优于串行流。
ArrayList 在并发修改下的 NoSuchElementException 问题
在使用 Iterator 遍历 ArrayList 时,如果在遍历过程中直接调用 list.remove() 而非 iterator.remove(),会抛出 ConcurrentModificationException。这是因为 ArrayList 维护了一个 modCount 字段来检测结构性修改,每次增删元素都会增加该计数,而 Iterator 在初始化时记录了预期的 modCount,两者不一致即报错。这是批量操作中常见的运行时异常,需通过正确的删除接口规避。



















