ArrayList 是 Java 中最常用的动态数组实现,其核心优势在于自动扩容——无需预先指定容量,即可按需增长。但这一“便利”背后,有一套严谨的扩容逻辑。
一、添加元素时触发扩容检查
当我们调用 add(E e) 方法时,ArrayList 并不会直接将元素放入数组,而是先检查容量是否足够:
public boolean add(E e) { ensureCapacityInternal(size + 1); // 检查并确保容量 elementData[size++] = e; // 赋值 return true; }
关键在 ensureCapacityInternal 方法。
二、扩容的核心逻辑
- 默认初始容量
若使用无参构造(new ArrayList<>()),底层elementData初始为一个空数组。首次添加元素时,会初始化为容量 10。 - 扩容规则当
size + 1 > 当前容量时,触发扩容:
int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容 1.5 倍
- 例如:当前容量 10 → 扩容后为 15;
- 若仍不够(如一次性添加大量元素),则直接扩容到所需最小容量。
- 数组拷贝
扩容通过Arrays.copyOf(elementData, newCapacity)实现,创建新数组并将旧数据复制过去。这是一个 O(n) 操作,代价较高。
三、性能启示
- 频繁扩容影响性能:每次扩容都涉及内存分配与数据拷贝;
- 建议预估容量:若已知元素数量,使用
new ArrayList<>(initialCapacity)初始化,避免多次扩容; - Guava 推荐:
Lists.newArrayListWithCapacity(100)可读性更佳。
四、示例对比
// 不推荐:多次扩容 List<String> list1 = new ArrayList<>(); for (int i = 0; i < 1000; i++) { list1.add("item" + i); } // 推荐:一次到位 List<String> list2 = new ArrayList<>(1000); for (int i = 0; i < 1000; i++) { list2.add("item" + i); }
后者避免了约 10 次扩容(10 → 15 → 22 → 33 → ... → 1000),显著提升效率。
总结
ArrayList 的扩容机制以 1.5 倍增长平衡空间与时间开销,但在性能敏感场景下,主动指定初始容量是最佳实践。理解其底层原理,有助于写出更高效、更稳定的代码。