一、前言
ArrayList
是 Java 集合框架中最常用的可变长数组实现,提供了随机访问(O(1) 时间复杂度)和按需扩容的能力。
它实现了 List
、RandomAccess
、Cloneable
、Serializable
等接口,拥有广泛应用场景,如缓存、队列、简单的数据聚合等。
本文是作者学习总结的文章,有错误的地方还请指出。
二、ArrayList 的类结构
继承与接口
编辑
- 继承自
AbstractList<E>
,实现了List<E>
接口。 - 实现了标记接口
RandomAccess
(支持快速随机访问)、Cloneable
(可克隆)、Serializable
(可序列化)
关键字段
arduino
// 初始容量大小
private static final int DEFAULT_CAPACITY = 10;
// 起始空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 底层数组缓存,容量由此决定
// transient 关键字保证数据不被序列化,后续会进行解释
transient Object[] elementData;
// 当前实际元素个数
private int size;
elementData
起始为空数组(DEFAULTCAPACITY_EMPTY_ELEMENTDATA
),真正容量延迟到首次添加元素时才设为默认值 10。size
记录已添加元素数,仅当size
改变时才修改。
三、底层数据结构
ArrayList
本质上是不断扩容的数组:
- 初始时
elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,容量 0。 - 首次
add
时,将容量设为DEFAULT_CAPACITY = 10
。 - 后续若需求超过当前容量,则按
old + (old >> 1)
(1.5 倍)扩容。
四、构造方法分析
arduino
// 空参构造方法
ublic ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 指定内存容量的构造方法
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);
}
// 一次性填充集合的构造方法
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
// 若 c.toArray() 返回类型不为 Object[],则拷贝确保类型
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
- 无参构造延迟到第一次添加元素时设默认容量 10。
- 指定容量构造直接分配,避免不必要扩容。
- 集合构造一次性填充,提高性能。
五、添加元素相关源码
scss
// 添加元素到数组列表中
public boolean add(E e) {
// 确保内部容量足够容纳新元素(size + 1)
ensureCapacityInternal(size + 1);
// 将元素放入数组末尾
elementData[size++] = e;
return true;
}
// 确保当前内部容量足够(实际入口)
private void ensureCapacityInternal(int minCapacity) {
// 如果当前数组是空数组(默认空数组),则取默认容量与期望容量的较大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
// 显式检查容量
ensureExplicitCapacity(minCapacity);
}
// 显式检查并扩容(若必要)
private void ensureExplicitCapacity(int minCapacity) {
// 结构性修改次数 +1(用于快速失败机制)
modCount++;
// 如果需要的容量 > 当前数组容量,触发扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// 扩容逻辑
private void grow(int minCapacity) {
// 旧容量
int oldCapacity = elementData.length;
// 新容量 = 旧容量 + 旧容量 / 2(即扩容1.5倍)
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果新容量还是小于所需最小容量,则直接使用所需最小容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量超过最大数组大小限制,使用更大的容量策略
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 复制原数组内容到新数组,并替换引用
elementData = Arrays.copyOf(elementData, newCapacity);
}
// 处理极大容量请求的情况
private static int hugeCapacity(int minCapacity) {
// 如果所需容量是负数,说明发生了整数溢出,抛出内存溢出异常
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// 如果所需容量大于最大数组限制(Integer.MAX_VALUE - 8),
// 则直接返回 Integer.MAX_VALUE,否则返回 MAX_ARRAY_SIZE
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
ensureCapacityInternal
负责触发扩容,modCount
用于 Fail‑Fast 检测。- 扩容策略:新容量 = 旧容量 + 旧容量/2(即 1.5 倍)。
- 扩容开销主要在
Arrays.copyOf
,需谨慎避免频繁扩容。
六、获取元素与范围校验
arduino
// 获取指定位置上的元素
public E get(int index) {
// 检查索引是否合法(是否在 0 到 size-1 之间)
rangeCheck(index);
// 安全地返回指定索引处的元素,类型转换为泛型 E
return (E) elementData[index];
}
// 检查访问索引是否合法的方法
private void rangeCheck(int index) {
// 如果索引小于 0 或者大于等于当前元素个数,说明越界了
if (index < 0 || index >= size)
// 抛出数组越界异常,并携带提示信息
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
rangeCheck(index)
是为了防止访问非法索引,保护程序稳定性。elementData
是一个Object[]
类型的数组,存储实际的数据;取出时强制转换为泛型类型E
。
七、删除元素源码分析
arduino
// 根据索引移除元素
public E remove(int index) {
// 检查索引是否合法
rangeCheck(index);
// 修改次数加1,用于fail-fast机制
modCount++;
// 保存旧值,用于返回
E oldValue = (E) elementData[index];
// 计算需要移动的元素个数(从 index+1 开始到最后一个元素)
int numMoved = size - index - 1;
// 如果有元素需要向前移动,则使用 System.arraycopy 进行数组搬移
if (numMoved > 0)
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
// 将最后一个元素置为 null,帮助 GC 回收
elementData[--size] = null;
return oldValue; // 返回被移除的元素
}
// 根据对象值移除元素
public boolean remove(Object o) {
// 处理 null 元素的情况(== 判断)
if (o == null) {
for (int index = 0; index < size; index++) {
if (elementData[index] == null) {
fastRemove(index); // 快速移除
return true;
}
}
} else {
// 非 null 情况,用 equals 判断是否相等
for (int index = 0; index < size; index++) {
if (o.equals(elementData[index])) {
fastRemove(index); // 快速移除
return true;
}
}
}
// 未找到元素,返回 false
return false;
}
// 快速移除指定索引的元素,不进行返回
private void fastRemove(int index) {
modCount++; // 修改次数加1
int numMoved = size - index - 1;
// 搬移后续元素覆盖当前索引位置
if (numMoved > 0)
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
// 将尾部置空并更新 size
elementData[--size] = null;
}
// 构造错误信息的方法
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size;
}
remove(int index)
:用于删除指定索引位置的元素,并返回旧值。remove(Object o)
:用于根据对象内容删除列表中第一个匹配的元素(支持 null 值)。fastRemove(int index)
:简化版本的移除操作,用于内部调用,不需要返回被删除的值。modCount++
是为了支持 Java 集合的快速失败机制(fail-fast)。
八、扩容机制详解
- 默认容量:10(第一次
add
时生效) - 触发条件:
minCapacity > elementData.length
- 扩容公式:
newCapacity = oldCapacity + (oldCapacity >> 1)
(1.5 倍),若不足以满足minCapacity
,则取minCapacity
- 最大容量:
Integer.MAX_VALUE - 8
,超过时抛OutOfMemoryError
或调整到最大值。 - 扩容为 O(1),大多数
add
操作只需常数时间。
九、Fail‑Fast 机制
ArrayList
的迭代器通过记录创建时的expectedModCount = modCount
,每次调用next()
或remove()
时比对modCount
,若不一致则抛出ConcurrentModificationException
,快速失败,避免不确定行为。- 这种机制,在多线程环境下不保证绝对检测,但对单线程错误用法能及时暴露。
十、和其他集合的对比
对比项 | ArrayList | LinkedList | Vector |
---|---|---|---|
底层结构 | 动态数组 | 双向链表 | 动态数组(同步) |
随机访问 | O(1) | O(n) | O(1) |
头/尾插入删除 | O(n) | O(1) | O(n) |
扩容策略 | 1.5 倍 | 不需扩容 | 2 倍(默认) |
线程安全 | 否 | 否 | 是(同步) |
适用场景 | 随机访问、读多写少 | 频繁增删(尤其是头部) | 需要线程安全但性能较低时 |
十一、常见面试题总结
-
ArrayList 默认容量是多少?
- 默认为 10,但延迟到首次
add
时才分配
- 默认为 10,但延迟到首次
-
扩容机制如何实现?
- 新容量 =
old + (old >> 1)
,不足时取minCapacity
,最大不超过Integer.MAX_VALUE - 8
- 新容量 =
-
为什么线程不安全?
- 内部无同步,且
modCount
检测不是原子操作;多线程并发增删会导致数据不一致。
- 内部无同步,且
-
如何实现线程安全的 List?
- 使用
Collections.synchronizedList(new ArrayList<>())
- 使用
CopyOnWriteArist(juc 包下的集合,采用写时复制的思想)
- 使用 Vector(性能低,内部使用的 synchronized 修饰的方法)
- 使用
-
Fail‑Fast 原理是什么?
- 通过
modCount
与迭代器中的expectedModCount
比对,检测到不匹配则抛ConcurrentModificationException
- 通过
-
数组为什么使用
transient
进行修饰?-
transient
的意思是:这个字段在对象被序列化时不会被序列化。 -
不希望数组被默认序列化
- 数组可能只用了一部分,但数组有10个元素,会把所有空位也一起序列化出去(不利于效率与资源节省)
-
自定义序列化更灵活
ArrayList
使用了自定义的writeObject()
和readObject()
方法,仅序列化实际有用的数据
-
总结一句话:
ArrayList
中的数组使用transient
修饰,是为了避免无效元素被序列化,提高性能,并通过自定义序列化方法控制存储内容。
-