4.ArrayList 扩容机制与 Fail-Fast 原理

ArrayList 扩容机制与 Fail-Fast 原理

🚀 高频指数:★★★★★
🎯 你将收获:ArrayList 内部结构、扩容策略、源码分析、fail-fast 机制与项目实践。


一、面试常见问法

💬 面试官:

  1. ArrayList 的默认容量是多少?
  2. 扩容时增长多少?
  3. 什么是 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 是动态数组,扩容靠复制,遍历要规矩。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

愤怒的代码

如果您有受益,欢迎打赏博主😊

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值