ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

深入Java集合框架:ArrayList源码解析(JDK 8)

2026/8/15 18:48:31 拓冰建站 浏览量
深入Java集合框架:ArrayList源码解析(JDK 8)

前言

最近在系统地复习 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, index+1, elementData, index, numMoved);// 将最后一个位置置为 null,方便 GC 回收elementData[--size] = null; return oldValue;
}

结论:删除元素同样需要移动数组,且最后一个元素会被显式设为 null,帮助垃圾回收器回收。

3. 获取与修改 get / set

public 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线程不安全的。在多线程环境下,可能会出现:

  1. 数据覆盖:多个线程同时写入,导致元素丢失。

  2. 并发修改异常:一个线程遍历,另一个线程修改。

  3. JDK 1.7 及之前 HashMap 的死循环问题(虽然 ArrayList 不会死循环,但数据一致性无法保证)。

解决方案

  • 使用 Collections.synchronizedList(new ArrayList<>())(方法级加锁,性能较差)。

  • 使用 CopyOnWriteArrayList(写时复制,读操作完全无锁,适合读多写少的高并发场景)。

写在最后

这是我个人源码阅读系列的第一篇。虽然只是分析了ArrayList,但感觉自己对Java集合的理解比之前扎实了很多。

源码并不可怕,只要带着问题一步步Debug进去,总能发现很多精妙的设计。

如果这篇笔记对你有帮助,点个赞鼓励一下吧~ 有任何疑问也欢迎在评论区一起讨论交流!