前言
最近在系统地复习 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 是线程不安全的。在多线程环境下,可能会出现:
-
数据覆盖:多个线程同时写入,导致元素丢失。
-
并发修改异常:一个线程遍历,另一个线程修改。
-
JDK 1.7 及之前 HashMap 的死循环问题(虽然 ArrayList 不会死循环,但数据一致性无法保证)。
解决方案:
-
使用
Collections.synchronizedList(new ArrayList<>())(方法级加锁,性能较差)。 -
使用
CopyOnWriteArrayList(写时复制,读操作完全无锁,适合读多写少的高并发场景)。
写在最后
这是我个人源码阅读系列的第一篇。虽然只是分析了ArrayList,但感觉自己对Java集合的理解比之前扎实了很多。
源码并不可怕,只要带着问题一步步Debug进去,总能发现很多精妙的设计。
如果这篇笔记对你有帮助,点个赞鼓励一下吧~ 有任何疑问也欢迎在评论区一起讨论交流!
