ArrayList 知识点详解
一、ArrayList 简介
1.1 什么是 ArrayList?
ArrayList 是 Java 集合框架中最常用的 List 实现类,它是一个动态数组,可以自动扩容。想象一下,普通的数组就像一个固定大小的盒子,一旦装满就不能再装了;而 ArrayList 就像一个会自动变大的魔法盒子,装满后会自动变大,永远不用担心装不下。
1.2 继承体系
Iterable(接口)
│
Collection(接口)
│
List(接口)
│
AbstractList(抽象类)
│
ArrayList(实现类)
ArrayList 实现的接口:
- List 接口:提供列表的基本操作
- RandomAccess 接口:标记接口,表示支持快速随机访问
- Cloneable 接口:支持克隆
- Serializable 接口:支持序列化
1.3 核心特点
| 特性 | 说明 |
|---|---|
| 底层结构 | 动态数组(Object[]) |
| 线程安全 | 不安全 |
| 默认容量 | 10 |
| 扩容比例 | 1.5 倍 |
| 是否有序 | 有序(按插入顺序) |
| 是否可重复 | 可重复 |
| 是否支持 null | 支持 |
| 随机访问 | O(1) 时间复杂度 |
二、核心数据结构与图解
2.1 底层数组结构
ArrayList 底层使用一个 Object 数组来存储元素:
┌─────────────────────────────────────────────────────────────┐
│ ArrayList 内部结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ elementData (Object[] 数组) │
│ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐ │
│ │ A │ B │ C │ D │ E │ null│ null│ null│ null│ │
│ └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘ │
│ [0] [1] [2] [3] [4] [5] [6] [7] [8] │
│ │
│ size = 5(实际元素个数) │
│ capacity = 9(数组总容量) │
│ │
└─────────────────────────────────────────────────────────────┘
说明:
- elementData:存储元素的数组
- size:实际存储的元素个数
- capacity:数组的总容量(elementData.length)
2.2 添加元素过程图解
初始状态:capacity = 5, size = 0
Step 1: 添加元素 "A"
┌─────┬─────┬─────┬─────┬─────┐
│ A │ null│ null│ null│ null│ size = 1
└─────┴─────┴─────┴─────┴─────┘
Step 2: 添加元素 "B"
┌─────┬─────┬─────┬─────┬─────┐
│ A │ B │ null│ null│ null│ size = 2
└─────┴─────┴─────┴─────┴─────┘
Step 3: 继续添加...直到 capacity = 5, size = 5
┌─────┬─────┬─────┬─────┬─────┐
│ A │ B │ C │ D │ E │ size = 5(满了!)
└─────┴─────┴─────┴─────┴─────┘
Step 4: 添加元素 "F",触发扩容
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ A │ B │ C │ D │ E │ F │ null│ null│ 新容量 = 5 * 1.5 = 7
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
[0] [1] [2] [3] [4] [5] [6] [7]
2.3 插入元素过程图解
在索引 2 的位置插入元素 "X"
操作前:
┌─────┬─────┬─────┬─────┬─────┐
│ A │ B │ C │ D │ E │
└─────┴─────┴─────┴─────┴─────┘
[0] [1] [2] [3] [4]
Step 1: 将索引 2 及之后的元素向后移动一位
┌─────┬─────┬─────┬─────┬─────┐
│ A │ B │ │ C │ D │ E │
└─────┴─────┴─────┴─────┴─────┴─────┘
[0] [1] [2] [3] [4] [5]
↑
空出位置
Step 2: 在索引 2 的位置放入 "X"
┌─────┬─────┬─────┬─────┬─────┬─────┐
│ A │ B │ X │ C │ D │ E │
└─────┴─────┴─────┴─────┴─────┴─────┘
[0] [1] [2] [3] [4] [5]
时间复杂度:O(n),因为需要移动元素
2.4 删除元素过程图解
删除索引 2 的元素
操作前:
┌─────┬─────┬─────┬─────┬─────┐
│ A │ B │ X │ C │ D │ E │
└─────┴─────┴─────┴─────┴─────┴─────┘
[0] [1] [2] [3] [4] [5]
Step 1: 将索引 3 及之后的元素向前移动一位
┌─────┬─────┬─────┬─────┬─────┐
│ A │ B │ C │ D │ E │ null │
└─────┴─────┴─────┴─────┴─────┴─────┘
[0] [1] [2] [3] [4] [5]
Step 2: size 减 1,最后一个位置置为 null(帮助 GC)
┌─────┬─────┬─────┬─────┬─────┐
│ A │ B │ C │ D │ E │
└─────┴─────┴─────┴─────┴─────┘
[0] [1] [2] [3] [4]
时间复杂度:O(n),因为需要移动元素
三、源码分析
3.1 核心成员变量
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 空数组实例(用于空构造函数)
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认空数组实例(用于默认构造函数)
// 与 EMPTY_ELEMENTDATA 区分开,以区分第一次添加元素时扩容多少
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 真正存储元素的数组
// transient 表示不参与序列化,ArrayList 自定义了序列化方法
transient Object[] elementData;
// 实际元素个数
private int size;
// 数组最大容量
// -8 是因为某些 JVM 会在数组头部存储一些信息
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}
3.2 构造方法
/**
* 无参构造函数
* 使用默认容量 10
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 指定初始容量的构造函数
* @param initialCapacity 初始容量
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 创建指定容量的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 容量为 0,使用空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 负数容量,抛出异常
throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
}
}
/**
* 使用其他集合初始化
* @param c 集合
*/
public ArrayList(Collection<? extends E> c) {
// 将集合转换为数组
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
// 复制数组
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// 空集合,使用空数组
elementData = EMPTY_ELEMENTDATA;
}
}
3.3 添加元素方法(核心)
/**
* 在末尾添加元素
* 时间复杂度:平均 O(1)
*/
public boolean add(E e) {
// 确保容量足够
ensureCapacityInternal(size + 1);
// 将元素放入数组末尾
elementData[size++] = e;
return true;
}
/**
* 在指定位置插入元素
* 时间复杂度:O(n)
*/
public void add(int index, E element) {
// 检查索引是否越界
rangeCheckForAdd(index);
// 确保容量足够
ensureCapacityInternal(size + 1);
// 将 index 及之后的元素向后移动一位
// System.arraycopy 是 native 方法,效率较高
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 在 index 位置放入新元素
elementData[index] = element;
size++;
}
/**
* 确保内部容量
*/
private void ensureCapacityInternal(int minCapacity) {
// 如果是默认空数组,取 DEFAULT_CAPACITY 和 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);
}
3.4 扩容方法(核心)
/**
* 扩容方法
* 这是 ArrayList 的核心方法之一
*/
private void grow(int minCapacity) {
// 旧容量
int oldCapacity = elementData.length;
// 新容量 = 旧容量 + 旧容量/2 = 旧容量 * 1.5
// 右移一位相当于除以 2
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果新容量仍然不够,直接使用需要的最小容量
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // 溢出
throw new OutOfMemoryError();
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) // 溢出
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
3.5 获取元素方法
/**
* 获取指定位置的元素
* 时间复杂度:O(1)
*/
public E get(int index) {
// 检查索引是否越界
rangeCheck(index);
// 直接通过数组下标访问
return elementData(index);
}
/**
* 通过下标访问元素
*/
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
/**
* 检查索引范围
*/
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
3.6 删除元素方法
/**
* 删除指定位置的元素
* 时间复杂度:O(n)
*/
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;
}
/**
* 删除指定元素(第一个匹配的)
* 时间复杂度:O(n)
*/
public boolean remove(Object o) {
if (o == null) {
// 删除 null 元素
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
// 删除非 null 元素
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
/**
* 快速删除(不检查索引)
*/
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index + 1, elementData, index,
numMoved);
elementData[--size] = null;
}
3.7 修改元素方法
/**
* 替换指定位置的元素
* 时间复杂度:O(1)
*/
public E set(int index, E element) {
// 检查索引是否越界
rangeCheck(index);
// 获取旧值
E oldValue = elementData(index);
// 设置新值
elementData[index] = element;
return oldValue;
}
3.8 其他常用方法
/**
* 返回元素个数
*/
public int size() {
return size;
}
/**
* 判断是否为空
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 判断是否包含指定元素
*/
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
/**
* 查找元素第一次出现的位置
*/
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i] == null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
/**
* 清空列表
*/
public void clear() {
modCount++;
// 将所有元素置为 null,帮助 GC
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
/**
* 将容量缩小为实际大小
*/
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
四、扩容机制详解
4.1 扩容时机
当添加元素时,如果当前数组已满(size == elementData.length),就会触发扩容。
4.2 扩容公式
newCapacity = oldCapacity + (oldCapacity >> 1)
即:新容量 = 旧容量 + 旧容量 / 2 = 旧容量 * 1.5
位运算解析
newCapacity = oldCapacity + (oldCapacity >> 1)
| 表达式 | 含义 |
|---|---|
oldCapacity >> 1 | 右移1位 = 除以2 |
oldCapacity + oldCapacity/2 | 原值 + 原值的一半 |
| 最终结果 | oldCapacity × 1.5 |
举例说明
假设原容量 oldCapacity = 10:
oldCapacity >> 1 = 10 >> 1 = 5 (10 ÷ 2 = 5)
newCapacity = 10 + 5 = 15 (10 × 1.5 = 15)
为什么用位运算?
- 性能更好 — 位运算比乘除法更快
- 避免浮点数 — 整数运算,无精度问题
- 向下取整 — 奇数时会自动向下取整(如
11 >> 1 = 5)
4.3 扩容示例
初始容量:10
第 1 次扩容:10 * 1.5 = 15
第 2 次扩容:15 * 1.5 = 22(取整)
第 3 次扩容:22 * 1.5 = 33
第 4 次扩容:33 * 1.5 = 49
...以此类推
4.4 扩容流程图
┌─────────────────┐
│ 添加元素请求 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ size + 1 是否 │
│ 超过容量? │
└────────┬────────┘
│
┌──────────────┴──────────────┐
│ 否 │ 是
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 直接添加元素 │ │ 触发扩容 │
└─────────────────┘ └────────┬────────┘
│
▼
┌─────────────────┐
│ 计算新容量 │
│ = old * 1.5 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 创建新数组 │
│ 复制旧数据 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 添加元素 │
└─────────────────┘
4.5 扩容的性能影响
// 假设要添加 100 万个元素
// 方式 1:不指定初始容量(多次扩容)
ArrayList<Integer> list1 = new ArrayList<>();
// 会经历多次扩容:10 -> 15 -> 22 -> 33 -> 49 -> 73 -> ...
// 方式 2:指定初始容量(一次扩容都不需要)
ArrayList<Integer> list2 = new ArrayList<>(1000000);
// 直接分配 100 万容量,不需要扩容
// 性能对比:
// 方式 1 需要进行多次数组复制,性能较差
// 方式 2 直接分配足够容量,性能最佳
五、时间复杂度分析
5.1 各种操作的时间复杂度
| 操作 | 方法 | 时间复杂度 | 说明 |
|---|---|---|---|
| 随机访问 | get(index) | O(1) | 直接通过数组下标访问 |
| 末尾添加 | add(e) | O(1) 平均 | 可能触发扩容,但平均还是 O(1) |
| 指定位置添加 | add(index, e) | O(n) | 需要移动元素 |
| 删除末尾 | remove(size-1) | O(1) | 直接删除 |
| 删除指定位置 | remove(index) | O(n) | 需要移动元素 |
| 查找元素 | indexOf(o) | O(n) | 需要遍历 |
| 是否包含 | contains(o) | O(n) | 基于 indexOf |
| 修改元素 | set(index, e) | O(1) | 直接通过下标修改 |
5.2 与 LinkedList 对比
ArrayList LinkedList
随机访问 O(1) ✅ O(n) ❌
末尾添加 O(1) 平均 ✅ O(1) ✅
头部添加 O(n) ❌ O(1) ✅
中间插入 O(n) ❌ O(n) ❌
内存占用 较小 ✅ 较大 ❌
缓存友好 好 ✅ 差 ❌
六、线程安全问题
6.1 为什么 ArrayList 不是线程安全的?
// 假设有两个线程同时对 ArrayList 进行操作
// 线程 1:添加元素
list.add("A");
// 线程 2:同时添加元素
list.add("B");
// 可能出现的问题:
// 1. 数据覆盖:两个线程同时写入同一位置
// 2. 数组越界:扩容过程中的并发问题
// 3. size 值不正确:多个线程同时修改 size
// 4. 扩容时数据丢失
6.2 线程安全的替代方案
// 方案 1:使用 Collections.synchronizedList
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
// 方案 2:使用 CopyOnWriteArrayList(读多写少场景推荐)
CopyOnWriteArrayList<String> copyOnWriteList = new CopyOnWriteArrayList<>();
// 方案 3:使用 Vector(不推荐,已过时)
Vector<String> vector = new Vector<>();
// 方案 4:手动加锁
List<String> list = new ArrayList<>();
synchronized (list) {
list.add("element");
}
七、最佳实践
7.1 初始化时指定容量
// ❌ 不推荐:使用默认容量,可能多次扩容
ArrayList<String> list1 = new ArrayList<>();
// ✅ 推荐:预估容量,避免扩容
ArrayList<String> list2 = new ArrayList<>(1000);
7.2 遍历方式选择
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
// 方式 1:普通 for 循环(推荐,适合需要索引的场景)
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// 方式 2:增强 for 循环(推荐,代码简洁)
for (String s : list) {
System.out.println(s);
}
// 方式 3:迭代器(推荐,适合需要删除元素的场景)
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
if ("B".equals(s)) {
iterator.remove(); // 安全删除
}
}
// 方式 4:forEach + Lambda(Java 8+,代码简洁)
list.forEach(System.out::println);
// ❌ 不推荐:遍历时直接删除元素
for (String s : list) {
if ("B".equals(s)) {
list.remove(s); // 会抛出 ConcurrentModificationException
}
}
7.3 大量数据删除优化
// ❌ 不推荐:逐个删除,效率低
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
list.add(i);
}
for (int i = 0; i < list.size(); i++) {
if (list.get(i) % 2 == 0) {
list.remove(i);
i--; // 需要回退索引
}
}
// ✅ 推荐:使用 removeIf(Java 8+)
list.removeIf(n -> n % 2 == 0);
// ✅ 推荐:使用迭代器删除
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
if (it.next() % 2 == 0) {
it.remove();
}
}
7.4 转换为数组
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
// 方式 1:返回 Object[] 数组
Object[] array1 = list.toArray();
// 方式 2:传入指定类型的数组(推荐)
String[] array2 = list.toArray(new String[0]);
// 方式 3:传入指定大小的数组
String[] array3 = list.toArray(new String[list.size()]);
八、常见面试题
8.1 ArrayList 和 Array 的区别?
| 特性 | Array | ArrayList |
|---|---|---|
| 大小 | 固定 | 动态 |
| 类型 | 可以是基本类型 | 只能是引用类型 |
| 功能 | 基本操作 | 丰富的 API |
| 性能 | 稍快 | 稍慢(封装开销) |
| 泛型 | 不支持 | 支持 |
8.2 ArrayList 和 LinkedList 的区别?
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组 | 双向链表 |
| 随机访问 | O(1) | O(n) |
| 插入删除 | O(n) | O(1)(已知位置) |
| 内存占用 | 较小 | 较大 |
| 缓存友好 | 是 | 否 |
| 实现接口 | List, RandomAccess | List, Deque |
8.3 为什么 ArrayList 的扩容是 1.5 倍?
- 权衡时间和空间:扩容太少需要频繁扩容,扩容太多浪费空间
- 经验值:1.5 倍是一个经验值,在实际使用中表现良好
- 数学计算:如果希望均摊成本最低,最佳扩容因子约为 1.618(黄金分割比),1.5 是一个接近的值
8.4 ArrayList 如何实现序列化?
// ArrayList 使用自定义的序列化方法
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
// 写入非静态和非瞬态字段
s.defaultWriteObject();
// 写入 size
s.writeInt(size);
// 只写入实际存储的元素,不写入整个数组
for (int i = 0; i < size; i++) {
s.writeObject(elementData[i]);
}
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// 读取非静态和非瞬态字段
s.defaultReadObject();
// 读取 size
int size = s.readInt();
// 创建数组并读取元素
elementData = new Object[size];
for (int i = 0; i < size; i++) {
elementData[i] = s.readObject();
}
}
为什么这样设计?
- elementData 用 transient 修饰,不参与默认序列化
- 只序列化实际存储的元素,不序列化整个数组
- 节省空间,避免序列化 null 元素
九、总结
9.1 适用场景
✅ 适合使用 ArrayList 的场景:
- 需要频繁随机访问元素
- 主要在末尾添加/删除元素
- 数据量已知或可预估
- 单线程环境
❌ 不适合使用 ArrayList 的场景:
- 需要频繁在中间插入/删除元素
- 需要线程安全
- 频繁在头部插入/删除元素(考虑 LinkedList)
9.2 核心要点
- 底层数组:ArrayList 底层是 Object[] 数组
- 动态扩容:自动扩容,扩容因子 1.5
- 随机访问:支持 O(1) 随机访问
- 线程不安全:多线程环境需要同步处理
- 序列化:自定义序列化,只存储实际元素
9.3 记忆口诀
ArrayList 要记牢,
动态数组效率高。
随机访问 O(1),
末尾添加也是妙。
中间插入要移动,
扩容 1.5 别忘掉。
线程不安全要注意,
初始化容量要预料。