4.ArrayList 扩容机制与 Fail-Fast 原理
ArrayList 扩容机制与 Fail-Fast 原理
🚀 高频指数:★★★★★
🎯 你将收获:ArrayList 内部结构、扩容策略、源码分析、fail-fast 机制与项目实践。
一、面试常见问法
💬 面试官:
- ArrayList 的默认容量是多少?
- 扩容时增长多少?
- 什么是 fail-fast?为什么会抛 ConcurrentModificationException?
这些问题考的是你对 集合源码与线程安全 的理解深度。
二、ArrayList 内部结构
ArrayList 是基于 动态数组实现 的顺序表,支持随机访问,插入删除效率低于 LinkedList。
🔹 源码定义(JDK 8)
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {transient Object[] elementData; // 存储元素private int size; // 实际元素个数
}
底层是一个
Object[]数组,随着元素增多自动扩容。
三、初始容量与构造方法
| 构造方式 | 初始容量 | 说明 |
|---|---|---|
| new ArrayList() | 0(懒加载) | 第一次添加时初始化为10 |
| new ArrayList(int initialCapacity) | 指定容量 | 避免多次扩容 |
| Arrays.asList() | 固定长度 | 不支持 add/remove |
JDK 7 以前默认初始化数组为 10;
JDK 8 之后延迟到第一次 add 时创建。
四、扩容机制详解
🔹 add() 方法核心流程
public boolean add(E e) {ensureCapacityInternal(size + 1); // 确保容量足够elementData[size++] = e;return true;
}
🔹 ensureCapacityInternal()
private void ensureCapacityInternal(int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {minCapacity = Math.max(10, minCapacity);}ensureExplicitCapacity(minCapacity);
}
🔹 真正扩容逻辑 grow()
private void grow(int minCapacity) {int oldCapacity = elementData.length;int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍扩容if (newCapacity < minCapacity)newCapacity = minCapacity;elementData = Arrays.copyOf(elementData, newCapacity);
}
✅ 结论:ArrayList 每次扩容 1.5 倍。
旧数组拷贝到新数组(系统级复制,O(n))。
五、容量设计的取舍
| 策略 | 优点 | 缺点 |
|---|---|---|
| 每次 +1 | 节省空间 | 频繁扩容、低效 |
| 每次 ×2 | 减少扩容次数 | 空间浪费大 |
| 每次 ×1.5(ArrayList 采用) | 平衡时间与空间 | 最优综合策略 |
📌 面试口诀:“空间换时间,一扩一半。”
六、fail-fast 原理
🔹 问题复现
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
for (String s : list) {if ("B".equals(s)) list.remove(s); // 抛异常
}
输出:
Exception in thread "main" java.util.ConcurrentModificationException
🔹 原理分析
ArrayList 内部维护一个 modCount(修改次数计数器)。
源码片段:
protected transient int modCount = 0;public E remove(int index) {rangeCheck(index);modCount++;// ...
}
迭代器初始化时,会记录一个期望值:
private class Itr implements Iterator<E> {int expectedModCount = modCount;
}
在遍历时校验:
final void checkForComodification() {if (modCount != expectedModCount)throw new ConcurrentModificationException();
}
🚩 核心机制:结构修改(增删元素)后 modCount 改变 → 触发异常。
七、解决 fail-fast 的三种方式
| 方案 | 写法 | 原理 |
|---|---|---|
| ✅ 使用 Iterator.remove() | 在迭代器内部安全删除 | 同步更新 expectedModCount |
| ✅ 使用 CopyOnWriteArrayList | 读写分离 | 写时复制、线程安全 |
| ✅ 使用并发集合类 | ConcurrentLinkedQueue 等 | 无结构修改异常 |
示例:
Iterator<String> it = list.iterator();
while (it.hasNext()) {if ("B".equals(it.next())) it.remove(); // ✅ 正确
}
八、面试官追问清单
| 问题 | 答题要点 |
|---|---|
| ArrayList 初始容量是多少? | 第一次添加时初始化为10 |
| 扩容策略是什么? | 每次扩容为原容量的1.5倍 |
| 为什么不是2倍? | 兼顾时间与空间效率 |
| fail-fast 是什么? | 迭代时结构被修改触发并发修改异常 |
| 如何避免? | 使用 Iterator.remove 或并发集合 |
九、项目实践建议
- 批量添加数据时先
ensureCapacity(size)避免多次扩容; - 高并发场景用
CopyOnWriteArrayList; - 频繁插入删除中间元素的场景使用
LinkedList; - 扩容会拷贝数组,应避免在大数据循环中反复
add()。
十、口诀记忆
☕️ “扩容一半,拷贝新家;遍历乱改,抛你异常。”
十一、小结
| 知识点 | 要点 |
|---|---|
| 底层结构 | Object[] 动态数组 |
| 默认容量 | 第一次添加时为10 |
| 扩容比例 | 原容量 × 1.5 |
| fail-fast 原理 | modCount 检测结构修改 |
| 解决方案 | Iterator.remove / 并发集合 |
✅ 一句话总结:
ArrayList 是动态数组,扩容靠复制,遍历要规矩。
