前言最近在系统地复习 Java 基础发现自己对集合框架的掌握一直停留在面试八股阶段背得出ArrayList基于数组、查询快增删慢但从来没真正点开过它的源码看一看。于是诞生了这一篇帖子跟着JDK 8的源码一步步搞清楚它底层到底是怎么玩的。这篇帖子就是我的学习记录既是给自己留个备忘也希望能为同样在啃源码的小伙伴提供一些参考。如果有理解不到位的地方欢迎大家在评论区指正一、ArrayList 继承体系与核心属性1. 类继承关系ArrayList位于java.util包下它的核心继承与实现关系如下继承AbstractList提供了 List 接口的骨架实现。实现List接口定义列表的操作规范。实现RandomAccess接口标记接口表明支持快速随机访问底层用for循环遍历比用迭代器快。实现Cloneable接口支持克隆浅拷贝。实现Serializable接口支持序列化。2. 核心成员变量打开ArrayList的源码我们会看到几个非常关键的属性// 默认初始容量 private static final int DEFAULT_CAPACITY 10; // 用于空实例的共享空数组带初始容量0时 private static final Object[] EMPTY_ELEMENTDATA {}; // 用于默认大小空实例的共享空数组无参构造时 // 和无参构造区分开以便在第一次添加元素时知道要扩容多少 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA {}; // 真正存放元素的数组缓冲区ArrayList 的底层核心 transient Object[] elementData; // 当前列表中实际存放的元素个数注意不是数组长度 private int size; // 数组能分配的最大大小 private static final int MAX_ARRAY_SIZE Integer.MAX_VALUE - 8;二、构造方法详解ArrayList提供了三种构造方法1. 无参构造最常用public ArrayList() { this.elementData DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }JDK 8 中无参构造并没有立刻初始化一个长度为 10 的数组而是先指向一个共享的空数组。真正的扩容发生在第一次添加元素时。2. 指定初始容量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. 包含指定集合public ArrayList(Collection? extends E c) { elementData c.toArray(); if ((size elementData.length) ! 0) { // c.toArray() 可能返回的不是 Object[] 类型需要做防御性拷贝 if (elementData.getClass() ! Object[].class) elementData Arrays.copyOf(elementData, size, Object[].class); } else { this.elementData EMPTY_ELEMENTDATA; } }三、动态扩容机制扩容是ArrayList最核心的机制发生在添加元素时。我们以add(E e)方法为入口1. add 方法入口public boolean add(E e) { // 确保内部容量足够这是扩容的核心判断 ensureCapacityInternal(size 1); // Increments modCount!! elementData[size] e; return true; }2. 扩容流程追踪private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private static int calculateCapacity(Object[] elementData, int minCapacity) { // 如果是无参构造后的第一次添加取默认容量 10 和 所需最小容量 的较大值 if (elementData DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } private void ensureExplicitCapacity(int minCapacity) { modCount; // 修改次数1为 Fail-Fast 机制服务 // 如果所需最小容量 当前数组长度执行扩容 if (minCapacity - elementData.length 0) grow(minCapacity); }3. grow() 方法真正的扩容逻辑private void grow(int minCapacity) { int oldCapacity elementData.length; // 新容量 旧容量 旧容量的一半位运算相当于 1.5 倍扩容 int newCapacity oldCapacity (oldCapacity 1); // 如果新容量仍然小于所需最小容量直接等于最小容量 if (newCapacity - minCapacity 0) newCapacity minCapacity; // 如果新容量超过了最大数组大小限制 if (newCapacity - MAX_ARRAY_SIZE 0) newCapacity hugeCapacity(minCapacity); // 拷贝原数组到新数组这是一个耗时的 O(n) 操作 elementData Arrays.copyOf(elementData, newCapacity); }扩容总结扩容倍数旧容量的1.5 倍oldCapacity 1。触发时机当size 1 elementData.length时。性能损耗扩容会触发Arrays.copyOf进行全量数据拷贝因此在能预估数据量时务必指定初始容量。四、核心方法源码剖析1. 指定位置插入 add(int index, E element)public void add(int index, E element) { rangeCheckForAdd(index); // 检查下标越界 ensureCapacityInternal(size 1); // 检查扩容 // 将 index 及其之后的所有元素向右移动一位O(n) 操作 System.arraycopy(elementData, index, elementData, index 1, size - index); elementData[index] element; size; }结论在ArrayList中间插入元素需要移动后续所有元素效率较低。2. 删除元素 remove(int index)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; }结论删除元素同样需要移动数组且最后一个元素会被显式设为null帮助垃圾回收器回收。3. 获取与修改 get / setpublic E get(int index) { rangeCheck(index); return elementData(index); // 直接通过数组下标访问O(1) } public E set(int index, E element) { rangeCheck(index); E oldValue elementData(index); elementData[index] element; // 直接替换O(1) return oldValue; }结论基于数组索引访问这也是ArrayList查询快的根本原因。五、线程安全问题ArrayList是线程不安全的。在多线程环境下可能会出现数据覆盖多个线程同时写入导致元素丢失。并发修改异常一个线程遍历另一个线程修改。JDK 1.7 及之前 HashMap 的死循环问题虽然 ArrayList 不会死循环但数据一致性无法保证。解决方案使用Collections.synchronizedList(new ArrayList())方法级加锁性能较差。使用CopyOnWriteArrayList写时复制读操作完全无锁适合读多写少的高并发场景。写在最后这是我个人源码阅读系列的第一篇。虽然只是分析了ArrayList但感觉自己对Java集合的理解比之前扎实了很多。源码并不可怕只要带着问题一步步Debug进去总能发现很多精妙的设计。如果这篇笔记对你有帮助点个赞鼓励一下吧 有任何疑问也欢迎在评论区一起讨论交流