ArrayList 底层原理:源码解析、扩容机制、删除逻辑、遍历陷阱、线程不安全深度剖析
ArrayList 是 Java 开发中最高频使用的 List 实现类底层基于动态数组实现。绝大多数开发者日常写业务代码天天在用但很多人只停留在 “查询快、增删慢” 这个表层结论。思考ArrayList 懒加载机制、扩容源码细节、modCount快速失败原理、数组拷贝开销、遍历删除的坑、为什么线程不安全、和 Vector/CopyOnWriteArrayList 的选型对比。本文结合 JDK8 源码从底层存储、构造器、新增、扩容、删除、遍历机制、常见陷阱、性能分析全方位讲解带你吃透 ArrayList。前言Java 集合框架中List 是有序、可重复的集合接口。常用实现类ArrayList、LinkedList、Vector。ArrayList动态 Object 数组随机访问性能优秀中间位置增删代价高非线程安全LinkedList双向链表随机查找慢头尾增删性能好非线程安全Vector古老的动态数组所有方法加synchronized线程安全并发性能差基本废弃。核心一句话ArrayList 本质就是自动扩容的数组。数组长度是容量 capacity集合内实际元素数量是 size二者不是一回事。一、底层存储结构与核心成员变量JDK8 源码// 默认初始容量10 private static final int DEFAULT_CAPACITY 10; // 空数组实例用于有参构造传入容量0时 private static final Object[] EMPTY_ELEMENTDATA {}; // 默认容量空数组用于无参构造懒加载标记 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA {}; // 真正存储元素的底层数组transient修饰不参与序列化 transient Object[] elementData; // 集合实际存储的元素个数不是数组长度 private int size; // 集合修改计数器新增、删除、清空都会用于快速失败fail-fast机制 protected transient int modCount 0;重点解读transient Object[] elementDatatransient关键字该字段不会被默认序列化。原因elementData 数组的长度往往大于实际元素 size数组后面都是空位置。如果直接序列化整个数组会序列化大量 null浪费 IO 和存储空间。 ArrayList 重写了writeObject()和readObject()序列化时只序列化size个有效元素。sizevselementData.lengthsize当前集合中有效元素数量elementData.length底层数组总长度也就是容量 capacity。例new ArrayList ()此时 elementData 是空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATAlength0size0第一次 add 之后数组扩容到 10length10size1。二、三大构造方法详解1. 无参构造new ArrayList()public ArrayList() { this.elementData DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }懒加载机制初始化时不会创建长度为 10 的数组仅仅赋值一个空数组常量。第一次调用 add () 的时候才会触发第一次扩容把数组容量扩容到 10。JDK7 之前无参构造直接创建长度 10 数组JDK8 改成懒加载节省内存集合创建出来不使用的时候不占用数组内存。2. 指定初始容量构造new ArrayList(int initialCapacity)public ArrayList(int initialCapacity) { if (initialCapacity 0) { this.elementData new Object[initialCapacity]; } else if (initialCapacity 0) { this.elementData EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException(Illegal Capacity: initialCapacity); } }业务开发最佳实践如果预估集合存放元素数量直接指定初始容量规避多次扩容带来的数组拷贝开销。注意传入负数直接抛出非法参数异常。3. 集合入参构造new ArrayList(Collection? extends E c)public ArrayList(Collection? extends E c) { Object[] a c.toArray(); if ((size a.length) ! 0) { if (c.getClass() ArrayList.class) { elementData a; } else { elementData Arrays.copyOf(a, size, Object[].class); } } else { elementData EMPTY_ELEMENTDATA; } }把其他集合转为 ArrayList。如果传入集合本身就是 ArrayList直接复用数组引用否则执行数组拷贝。三、add 添加元素 核心扩容机制重点add (E e) 尾部添加源码public boolean add(E e) { ensureCapacityInternal(size 1); // 检查是否需要扩容 elementData[size] e; return true; }ensureCapacityInternal(size 1)判断添加当前元素后是否超出数组容量需要扩容elementData[size] e元素放到数组下标 size 位置size 自增。扩容完整调用链ensureCapacityInternal→ensureExplicitCapacity→grow(minCapacity)private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private static int calculateCapacity(Object[] elementData, int minCapacity) { // 如果是无参构造的空数组第一次add最小需要容量取默认容量10 if (elementData DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } private void ensureExplicitCapacity(int minCapacity) { modCount; // 集合修改次数1 // 最小需要容量 底层数组长度触发扩容 if (minCapacity - elementData.length 0) grow(minCapacity); } // 真正扩容方法 private void grow(int minCapacity) { int oldCapacity elementData.length; // 新容量 oldCapacity oldCapacity 1也就是原容量 * 1.5倍 int newCapacity oldCapacity (oldCapacity 1); // 如果扩容1.5倍仍然不够直接使用minCapacity if (newCapacity - minCapacity 0) newCapacity minCapacity; // 容量上限判断最大数组容量 Integer.MAX_VALUE - 8 if (newCapacity - MAX_ARRAY_SIZE 0) newCapacity hugeCapacity(minCapacity); // 数组拷贝创建新数组复制旧数组全部元素 elementData Arrays.copyOf(elementData, newCapacity); }扩容要点总结扩容倍数1.5 倍old 1 等价除以 2扩容本质新建一个更大的数组Arrays.copyOf 拷贝旧数组所有元素到新数组数组拷贝是内存复制操作数据量大的时候性能开销不可忽视最大容量限制MAX_ARRAY_SIZE Integer.MAX_VALUE - 8防止 OOM。举例初始容量 10。当 size10再次 addminCapacity11旧容量 10新容量 15。数组扩容到 15。add (int index, E element) 指定位置插入public void add(int index, E element) { rangeCheckForAdd(index); // 下标合法性校验 ensureCapacityInternal(size 1); modCount; // 将index以及后面所有元素向后移动一位空出index位置 System.arraycopy(elementData, index, elementData, index 1, size - index); elementData[index] element; size; }指定下标插入需要调用System.arraycopy把后面元素整体后移时间复杂度 O (n)。这就是 ArrayList 中间插入慢的根本原因大量元素移动。四、remove 删除元素源码内存释放细节两种删除方式按下标删除remove(int index)、按元素删除remove(Object o)public E remove(int index) { rangeCheck(index); // 下标校验 modCount; E oldValue elementData(index); int numMoved size - index - 1; if (numMoved 0) // index后面元素向前移动一位覆盖被删除元素 System.arraycopy(elementData, index1, elementData, index, numMoved); // 数组最后一个有效位置置null断开对象引用帮助GC回收防止内存泄漏 elementData[--size] null; return oldValue; }关键点删除中间元素后续元素向前拷贝移动O (n)elementData[--size] null;非常关键手动置空引用。 如果不置 null底层数组还保留对象引用即使业务没有引用对象也无法被 GC造成内存泄漏。remove(Object o)遍历数组找到第一个匹配元素调用上面的删除逻辑。只能删除第一个匹配元素。五、modCount 与 Fail-Fast 快速失败机制modCount记录集合结构性修改次数add、remove、clear 都会让 modCountset 修改指定下标元素不会修改 modCount。ArrayList 迭代器Iterator创建时会保存当前modCount到expectedModCount。 迭代遍历过程中每次 next ()/remove () 都会校验if (modCount ! expectedModCount) { throw new ConcurrentModificationException(); }一旦发现外部修改集合立刻抛出ConcurrentModificationException也就是快速失败。重要误区快速失败不是线程安全方案它仅仅是一个检测机制用来快速暴露并发修改问题不能保证数据正确性。并发场景下异常不一定抛出可能出现数据丢失。经典坑增强 for 循环遍历执行 removeListString list new ArrayList(); list.add(A); list.add(B); list.add(C); //错误写法大概率抛出ConcurrentModificationException for(String str : list){ if(B.equals(str)){ list.remove(str); } }增强 for 循环底层就是 Iterator 迭代器。循环内直接调用 list.remove ()modCount 增加和 expectedModCount 不一致抛出异常。三种正确方案1.使用迭代器自带iterator.remove()IteratorString it list.iterator(); while(it.hasNext()){ String s it.next(); if(B.equals(s)){ it.remove(); } }iterator.remove () 会同步更新 expectedModCount不会触发异常。2.Java8 使用list.removeIf()推荐简洁list.removeIf(item - B.equals(item));3.反向 for 循环遍历删除不推荐可读性差六、常用 API 与时间复杂度汇总方法描述时间复杂度备注get(int index)根据下标获取元素O(1)随机访问数组直接寻址add(E e)尾部新增元素O (1)不扩容扩容触发数组拷贝则 O (n)add(int index,E e)指定下标插入O(n)需要移动后续元素remove(int index)按下标删除O(n)需要移动后续元素size()获取元素个数O(1)直接返回成员变量 sizeset(int index,E e)修改指定下标元素O(1)不修改 modCountcontains(Object o)判断是否包含元素O(n)底层遍历数组 equals 对比七、ArrayList 线程不安全深度分析ArrayList 所有方法没有加任何锁多线程并发读写会出现数据错乱。场景 1多线程并发 add两个线程同时执行 add线程 A 执行elementData[size] esize 还没自增完成切换时间片线程 B 也写入同一个下标位置覆盖 A 写入的数据size 只自增一次最终元素丢失。场景 2并发遍历 修改触发快速失败ConcurrentModificationException。注意异常不是每次必现属于并发竞态条件偶现问题线上很难复现。并发场景替代方案对比Collections.synchronizedList(new ArrayList())方法加同步锁全表锁并发性能差CopyOnWriteArrayList写时复制。写操作拷贝一份新数组读不加锁。读多写少场景推荐写多场景每次写拷贝大数组内存开销巨大。选型读多写少并发访问 → CopyOnWriteArrayList写多并发场景不推荐。八、开发实战踩坑清单大量插入数据不指定初始容量频繁触发扩容 数组拷贝严重影响性能。预估元素数量 N创建new ArrayList(N)。遍历删除使用增强 for 循环引发并发修改异常。优先removeIf。把 ArrayList 作为方法返回值直接暴露外部可以修改集合破坏封装。可以返回不可变集合Collections.unmodifiableList()。集合存储大量对象删除元素后忘记切断引用引发内存泄漏ArrayList 删除已经做了置 null但是业务代码要注意对象引用。不要用 ArrayList 做频繁中间插入、删除操作大量元素移动性能差优先 LinkedList。subList () 返回的不是新 ArrayList是原集合的视图对子列表修改会影响原 ArrayList容易踩坑。ListString subList list.subList(0,2); subList.add(test); // 原list也会新增元素九、ArrayList VS LinkedList VS Vector对比项ArrayListLinkedListVector底层结构动态数组双向链表动态数组随机访问O (1)支持O (n)不支持O(1)中间增删O (n)元素移动O (1)找到节点后O(n)线程安全不安全不安全安全方法加 synchronized扩容1.5 倍不需要扩容2 倍适用场景读多写少随机查询多频繁头尾增删查询少基本淘汰十、问答Q1ArrayList 默认初始容量JDK8 是懒加载吗扩容机制A默认容量 10。JDK8 无参构造是懒加载创建对象不初始化数组第一次 add 才扩容到 10每次扩容为原来 1.5 倍底层依靠 Arrays.copyOf 拷贝数组。Q2elementData 为什么用 transient 修饰Aelement 数组包含大量空占位元素不需要全部序列化。ArrayList 重写 writeObject只序列化 size 个有效元素节省空间。Q3什么是快速失败 fail-fast原理是什么是线程安全吗A迭代时对比 modCount 和 expectedModCount不一致抛出 ConcurrentModificationException。只是并发修改检测机制不是线程安全不能保证并发下数据正确。Q4ArrayList.subList () 坑点AsubList 返回视图不是新集合。修改子列表会影响原集合。如果需要独立集合需要 new ArrayList(subList)。Q5ArrayList 线程不安全并发场景怎么选A读多写少用 CopyOnWriteArrayList如果读写都频繁自己加锁控制Vector 性能差不推荐。结语ArrayList 看着简单但是底层数组拷贝、懒加载、快速失败、subList 视图、序列化细节都是面试高频考点。它性能优缺点根源都来自底层数组结构。掌握 ArrayList 底层后续学习 HashMap、数组相关集合会更容易理解。 实际开发中不要只凭惯性使用根据读写场景做合理选型规避扩容、遍历删除等常见坑。