在 Java 集合框架中,ArrayList是开发中使用频率最高的集合类之一,它以动态数组的特性、高效的随机访问能力成为 List 接口的主流实现。
一、ArrayList 基础定义
ArrayList 是Java 集合框架 下java.util包中的动态数组 实现类,它实现了List接口,底层基于Object 类型的数组存储数据。
核心特性:
- 动态扩容:无需手动指定容量,集合会根据元素数量自动扩容数组长度;
- 有序可重复:元素存储顺序与插入顺序一致,允许存入 null 值和重复元素;
- 非线程安全:多线程环境下使用会出现线程安全问题;
- 支持随机访问:通过索引可以 O (1) 时间复杂度直接访问元素。
二、ArrayList 的继承与实现关系
ArrayList 的类定义源码如下,清晰展示了其继承体系:
java
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
核心继承 / 实现关系解析
- 继承
AbstractList:抽象父类,实现了 List 接口的通用方法(如add、remove),减少 ArrayList 的重复代码; - 实现
List接口:定义了列表的核心规范(增删改查、遍历等); - 实现
RandomAccess接口 :标记型接口 ,表示 ArrayList 支持快速随机访问,遍历优先使用普通 for 循环; - 实现
Cloneable接口:支持克隆(浅克隆); - 实现
Serializable接口:支持序列化,可在网络传输、本地存储中使用。
关键:
RandomAccess是空接口,仅作为标识,JDK 会根据该接口优化集合遍历方式。
三、ArrayList 源码解析
要理解 ArrayList,必须先理解其底层存储结构、核心常量、构造方法、扩容机制,这是原理的核心。
1. 核心成员变量
java
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
// 序列化版本号
private static final long serialVersionUID = 8683452581122863359L;
/**
* 默认初始容量:创建ArrayList时不指定容量,默认容量为10
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 空数组实例:用于无参构造时的初始存储
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 空数组实例:用于指定容量为0时的存储
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 底层核心存储:ArrayList真正存放数据的Object数组
* 非私有,方便嵌套类访问
*/
transient Object[] elementData;
/**
* ArrayList中实际存储的元素个数(不是数组长度)
*/
private int size;
}
关键变量区分
elementData:底层数组,长度 = 集合容量;size:实际元素数量,永远 ≤ elementData.length;DEFAULT_CAPACITY:默认初始容量 10。
2. 三大构造方法
ArrayList 提供 3 种构造方式,决定了底层数组的初始化逻辑:
(1)无参构造(最常用)
java
public ArrayList() {
// 初始化为空数组,第一次添加元素时才会扩容为默认容量10
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
重点:无参构造不会直接创建长度为 10 的数组,懒加载初始化,节省内存。
(2)指定初始容量的构造
java
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 创建指定长度的Object数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 容量为0,赋值为空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
}
}
优化建议:预知元素数量时,指定初始容量,避免多次扩容,提升性能。
(3)集合参数构造
java
public ArrayList(Collection<? extends E> c) {
// 将集合转为数组
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// 兼容JDK版本,确保数组类型为Object[]
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 传入空集合,赋值为空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
3. 核心机制:动态扩容(源码)
ArrayList 的动态扩容 是其核心特性,当底层数组存满元素时,会自动创建新数组并复制数据。扩容入口:add方法添加元素前,会检查容量,不足则触发扩容。
步骤 1:add () 方法(添加元素)
java
public boolean add(E e) {
// 检查容量,不足则扩容
ensureCapacityInternal(size + 1);
// 元素存入数组,size自增
elementData[size++] = e;
return true;
}
步骤 2:ensureCapacityInternal () 计算容量
java
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;
}
步骤 3:ensureExplicitCapacity () 判断是否扩容
java
private void ensureExplicitCapacity(int minCapacity) {
// 修改次数+1(用于迭代器快速失败)
modCount++;
// 最小需要容量 > 底层数组长度 → 触发扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
步骤 4:grow () 真正扩容(核心)
java
private void grow(int minCapacity) {
// 原数组容量
int oldCapacity = elementData.length;
// 新容量 = 原容量 + 原容量/2 → 1.5倍扩容
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 特殊情况:新容量 < 最小需要容量(如初始化时)
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 超过最大数组容量,使用Integer.MAX_VALUE
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 数组复制:创建新数组,将原数组数据复制到新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
扩容核心总结
- 扩容倍数 :默认1.5 倍扩容 (
oldCapacity >> 1等价于除以 2,位运算效率更高); - 触发时机 :添加元素时,
size+1 > 数组容量; - 底层实现 :通过
Arrays.copyOf创建新数组 + 复制数据(底层调用System.arraycopy本地方法,效率较高); - 缺点:频繁扩容会创建新数组、复制数据,消耗性能,因此建议提前指定容量。
4. 核心方法源码解析
(1)get (int index) 按索引获取元素
java
public E get(int index) {
// 检查索引是否越界
rangeCheck(index);
// 直接返回数组对应索引元素,O(1)时间复杂度
return elementData(index);
}
// 索引越界校验
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
// 数组元素获取(强转泛型)
E elementData(int index) {
return (E) elementData[index];
}
优势:随机访问效率极高,是 ArrayList 最大的优点。
(2)set (int index, E element) 修改元素
java
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
// 直接替换数组元素
elementData[index] = element;
return oldValue;
}
(3)remove (int index) 删除元素
java
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
// 计算需要移动的元素个数
int numMoved = size - index - 1;
if (numMoved > 0)
// 数组拷贝:将后面的元素向前移动一位
System.arraycopy(elementData, index+1, elementData, index, numMoved);
// 最后一位置空,帮助GC回收
elementData[--size] = null;
return oldValue;
}
缺点:删除非末尾元素,需要移动数组元素,效率低。
(4)clear () 清空集合
java
public void clear() {
modCount++;
// 所有元素置空,GC回收
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
四、ArrayList 迭代器原理
ArrayList 提供两种迭代器:Iterator和ListIterator,核心实现是内部类Itr和ListItr。
1. 迭代器核心源码(Itr)
java
private class Itr implements Iterator<E> {
int cursor; // 下一个要返回的元素索引
int lastRet = -1; // 上一个返回的元素索引
int expectedModCount = modCount; // 快速失败标记
// 判断是否有下一个元素
public boolean hasNext() {
return cursor != size;
}
// 获取下一个元素
public E next() {
checkForComodification();
int i = cursor;
if (i >= size) throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
cursor = i + 1;
return (E) elementData[lastRet = i];
}
// 快速失败校验:迭代时修改集合,抛出ConcurrentModificationException
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
2. 特性:快速失败(Fail-Fast)
modCount:集合修改次数(增 / 删 / 清空都会 + 1);expectedModCount:迭代器初始化时复制modCount;- 原理:迭代过程中,如果集合被外部线程 / 代码修改 ,
modCount会变化,与expectedModCount不相等,立即抛出ConcurrentModificationException,避免遍历数据不一致。
3. 迭代器使用注意事项
- 迭代时不能通过集合的 add/remove 方法修改元素,只能用迭代器的 remove 方法;
- 多线程环境下,迭代器会触发快速失败,无法保证线程安全。
五、ArrayList 与 LinkedList 对比
两者都是 List 接口实现,但底层结构、性能、适用场景天差地别:
| 维度 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态 Object 数组 | 双向链表 |
| 随机访问 | 支持,O (1) 效率高 | 不支持,O (n) 效率低 |
| 插入 / 删除元素 | 末尾操作 O (1),中间 O (n) | 任意位置 O (1),效率高 |
| 内存占用 | 内存连续,扩容会浪费空间 | 每个节点存数据 + 前后指针,占用更多内存 |
| 线程安全 | 非线程安全 | 非线程安全 |
| 适用场景 | 大量查询、读取操作 | 大量插入、删除操作 |
| 扩容机制 | 1.5 倍动态扩容 | 无需扩容,链表无限扩展 |
总结
- 读多写少 → 选ArrayList(开发主流选择);
- 写多读少 → 选LinkedList;
- 两者均非线程安全,多线程用
CopyOnWriteArrayList。
六、ArrayList 面试高频考点
1. ArrayList 的底层原理?
基于Object 数组 实现,支持1.5 倍动态扩容,非线程安全的动态数组,支持快速随机访问。
2. 无参构造的 ArrayList 初始容量是多少?
无参构造初始化时是空数组 ,第一次添加元素时才会扩容为默认容量 10(懒加载)。
3. ArrayList 扩容机制?
添加元素时容量不足,会创建1.5 倍原容量 的新数组,通过System.arraycopy复制原数组数据。
4. 为什么 ArrayList 查询快,增删慢?
- 查询:数组支持索引随机访问,O (1) 时间复杂度;
- 增删:中间位置增删需要移动后续所有元素,O (n) 时间复杂度。
5. ArrayList 迭代时为什么不能修改集合?
因为快速失败机制 ,迭代器会校验modCount,集合被修改后抛出异常,防止遍历数据错乱。
6. ArrayList 和数组的区别?
- 数组:长度固定,只能存同类型数据,无内置方法;
- ArrayList:动态扩容,支持泛型,提供丰富的增删改查方法。
7. ArrayList 线程安全吗?如何实现线程安全?
- 不安全;
- 解决方案:
- 使用
Vector(低效,不推荐); Collections.synchronizedList()包装;- 推荐:JUC 包下的
CopyOnWriteArrayList。
- 使用
8. ArrayList 为什么用 transient 修饰 elementData?
elementData是底层数组,数组长度(容量)大于实际元素数量(size),序列化时直接序列化数组会浪费空间。ArrayList 重写writeObject方法,只序列化实际元素,节省空间。
七、总结
ArrayList 作为 Java 最常用的集合,核心是动态数组 + 1.5 倍扩容 + 随机访问,它的优势在于查询读取,劣势在于中间位置增删。
开发中遵循两个优化原则:
- 预知元素数量时,指定初始容量,避免频繁扩容;
- 读多写少场景优先使用 ArrayList,写多读少使用 LinkedList。
