深度拆解 ArrayList 源码:从动态扩容到对象拷贝的艺术
前言
ArrayList 几乎是每个 Java 程序员每天都要打交道的类。面试官最喜欢问:“它是怎么扩容的?”或者“它的初始化容量是多少?”。今天我们放下课本,直接看代码,把这个面试重灾区彻底吃透。
一、 核心成员变量:ArrayList 的骨架
打开 ArrayList 的源码,你会看到几个关键角色:
// 默认的初始容量为 10
private static final int DEFAULT_CAPACITY = 10;
// 用于空实例的共享空数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};
// 存储元素的缓冲区(这就是 ArrayList 的本质:数组)
transient Object[] elementData;
// 包含的元素数量
private int size;
关键点:
-
transient:
elementData被标记为transient,意味着它不会被默认的序列化机制处理。ArrayList内部通过writeObject和readObject自定义了序列化逻辑,只序列化实际有数据的部分,节省空间。 -
Object[]:这就是为什么
ArrayList能存任何对象,但在取出来时需要类型转换(泛型在编译后会擦除)。
二、 初始化:懒加载的智慧
在 JDK 1.8 之后,当你执行 new ArrayList() 时,它并没有立即创建一个长度为 10 的数组。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
此时 elementData 是一个空数组。真正的容量分配发生在第一次调用 add() 方法时。这种“延时加载”的设计避免了创建大量空集合导致的内存浪费。
三、 扩容机制:核心算法解析(重点)
当我们调用 add() 时,ArrayList 会先检查容量是否足够。
1. 扩容入口
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 如果是第一次添加,取 10 和 minCapacity 的最大值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
2. 核心 grow 方法
当空间不足时,会触发 grow(int minCapacity) 方法:
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 核心扩容逻辑:新容量 = 旧容量 + 旧容量 / 2 (即 1.5 倍)
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 检查新容量是否满足最小需求
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 检查是否超过最大限制 (MAX_ARRAY_SIZE)
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 拷贝原数据到新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
面试大白话: 扩容就像是搬家。原先的房子(数组)住不下了,我们找一个 1.5 倍大的新房子,把家具(元素)全部搬过去(System.arraycopy),最后把老房子拆了(旧数组等待 GC 回收)。
四、 核心方法源码细节
1. add(E e) —— 尾部插入
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确保容量
elementData[size++] = e; // 直接在末尾赋值
return true;
}
-
复杂度:平均 O(1),除非触发扩容。
2. add(int index, E element) —— 指定位置插入
public void add(int index, E element) {
rangeCheckForAdd(index); // 检查索引越界
ensureCapacityInternal(size + 1); // 确保容量
// 将 index 之后的元素全部向后挪一位
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element; // 插入新元素
size++;
}
-
复杂度:O(n)。这就是为什么说
ArrayList在中间插入元素效率低的原因。
五、 关于 System.arraycopy() 和 Arrays.copyOf()
在源码中,这两个方法频繁出现:
-
Arrays.copyOf():主要用于扩容。它内部会创建一个新数组,然后调用System.arraycopy把数据填进去。 -
System.arraycopy():这是个 native 方法(底层由 C/C++ 实现),用于在同一个数组或不同数组间移动数据。它的性能极高,远快于我们手写for循环。
六、 进阶思考:为什么 ArrayList 线程不安全?
在 add() 方法中:
-
elementData[size++] = e;这行代码不是原子的。 -
它分为两步:先在
size处赋值,再执行size = size + 1。
并发风险: 如果线程 A 和线程 B 同时执行。A 刚写完值还没来得及 size++,B 进来了,也往同一个 size 位置写,就会导致 A 的数据被覆盖。甚至可能因为 size 还没更新,导致数组下标越界。
七、 总结:如何优雅地使用 ArrayList?
-
预估容量:如果知道要存 500 个元素,请用
new ArrayList<>(500)。这样可以避免约 7-8 次的扩容和数组拷贝。 -
谨慎删除:如果需要大量删除元素,建议从后往前删,或者直接使用
removeIf(Java 8+)。 -
多线程避坑:在并发环境下,请使用
CopyOnWriteArrayList或Collections.synchronizedList。
结语: 源码是最好的老师。通过今天的分析,相信你对 ArrayList 的扩容、性能瓶颈以及底层数组操作有了全新的认识。
如果你觉得这篇源码分析够干货,别忘了收藏转发!
更多推荐



所有评论(0)