文章目录
-
- 前言
- [一、认识 ArrayList](#一、认识 ArrayList)
-
- [1.1 ArrayList 是什么](#1.1 ArrayList 是什么)
- [1.2 ArrayList 的主要特点](#1.2 ArrayList 的主要特点)
- [1.3 ArrayList 与普通数组的区别](#1.3 ArrayList 与普通数组的区别)
- [1.4 ArrayList 的适用场景](#1.4 ArrayList 的适用场景)
- 二、创建方式与底层结构
-
- [2.1 ArrayList 的创建方式](#2.1 ArrayList 的创建方式)
-
- [1. 无参构造](#1. 无参构造)
- [2. 指定初始容量](#2. 指定初始容量)
- [3. 根据已有集合创建](#3. 根据已有集合创建)
- [2.2 ArrayList 的核心成员变量](#2.2 ArrayList 的核心成员变量)
- [2.3 size 与 capacity 的区别](#2.3 size 与 capacity 的区别)
- [2.4 为什么底层是 Object 数组](#2.4 为什么底层是 Object 数组)
- [2.5 ArrayList 保存的是对象引用](#2.5 ArrayList 保存的是对象引用)
- 三、添加元素与扩容机制
-
- [3.1 add(E e) 添加元素](#3.1 add(E e) 添加元素)
- [3.2 无参构造与懒初始化](#3.2 无参构造与懒初始化)
- [3.3 第 11 个元素为什么会触发扩容](#3.3 第 11 个元素为什么会触发扩容)
- [3.4 为什么不能每次只扩大一个位置](#3.4 为什么不能每次只扩大一个位置)
- [3.5 Arrays.copyOf() 的作用](#3.5 Arrays.copyOf() 的作用)
- [3.6 ensureCapacity() 与 trimToSize()](#3.6 ensureCapacity() 与 trimToSize())
- 四、增删改查与元素移动
-
- [4.1 get() 与 set():下标访问为什么快](#4.1 get() 与 set():下标访问为什么快)
- [4.2 indexOf()、lastIndexOf() 与 contains()](#4.2 indexOf()、lastIndexOf() 与 contains())
- [4.3 add(index, element):指定位置插入元素](#4.3 add(index, element):指定位置插入元素)
- [4.4 remove():删除元素为什么可能较慢](#4.4 remove():删除元素为什么可能较慢)
- [4.5 Integer 列表中的 remove() 陷阱](#4.5 Integer 列表中的 remove() 陷阱)
- 五、遍历、排序与安全删除
-
- [5.1 ArrayList 的常见遍历方式](#5.1 ArrayList 的常见遍历方式)
-
- [1. 普通 for 循环](#1. 普通 for 循环)
- [2. 增强 for 循环](#2. 增强 for 循环)
- [3. Iterator 迭代器](#3. Iterator 迭代器)
- [5.2 遍历过程中直接删除的问题](#5.2 遍历过程中直接删除的问题)
- [5.3 正确删除方式](#5.3 正确删除方式)
-
- [1. 使用 Iterator.remove()](#1. 使用 Iterator.remove())
- [2. 使用 removeIf()](#2. 使用 removeIf())
- [5.4 排序与二分查找](#5.4 排序与二分查找)
- 六、常见陷阱与使用选择
-
- [6.1 Arrays.asList() 不能直接增删元素](#6.1 Arrays.asList() 不能直接增删元素)
- [6.2 subList() 返回的是视图](#6.2 subList() 返回的是视图)
- [6.3 clone() 只是浅拷贝](#6.3 clone() 只是浅拷贝)
- [6.4 ArrayList 不是线程安全集合](#6.4 ArrayList 不是线程安全集合)
- [6.5 ArrayList 与 LinkedList 的选择](#6.5 ArrayList 与 LinkedList 的选择)
- [6.6 常见操作时间复杂度](#6.6 常见操作时间复杂度)
- 七、完整测试代码
-
- [7.1 测试内容说明](#7.1 测试内容说明)
- [7.2 完整可运行代码](#7.2 完整可运行代码)
- 总结
前言
在 Java 开发中,ArrayList 是最常用的集合类之一。查询结果、商品列表、评论列表、菜单选项等业务数据,经常都会使用它来保存。
普通数组就像一排固定数量的座位 :创建时有多少位置,后续就只能使用多少位置。ArrayList 则像一节可以不断扩建的车厢:底层仍然使用数组,但空间不足时,会自动创建更大的数组,并把原有元素搬过去。
本文从基础使用开始,逐步讲解 ArrayList 的底层结构、扩容机制、增删改查原理、遍历删除陷阱、线程安全问题以及常见使用误区。
本文源码分析主要以 JDK 8 为基础。较新的 JDK 对部分内部方法进行了重构,但 ArrayList 的核心思想并没有改变:
text
底层是数组
容量不足时需要扩容
根据下标访问速度快
中间插入和删除需要移动元素
一、认识 ArrayList
1.1 ArrayList 是什么
ArrayList<E> 是 List<E> 接口的一个实现类,底层通过数组保存元素。
java
List<String> names = new ArrayList<>();
这段代码中:
java
List<String> names = new ArrayList<>();
可以拆解为:
| 内容 | 说明 |
|---|---|
List |
列表接口,表示一组有顺序的数据 |
ArrayList |
基于数组实现的列表类 |
String |
集合中存放的元素类型 |
<> |
泛型,用于限制元素类型 |
例如:
java
List<String> names = new ArrayList<>();
names.add("张三");
names.add("李四");
names.add("王五");
System.out.println(names);
输出结果:
text
[张三, 李四, 王五]
1.2 ArrayList 的主要特点
ArrayList 具有以下特点:
| 特点 | 说明 |
|---|---|
| 有序 | 元素按照添加顺序保存 |
| 可重复 | 可以保存相同元素 |
允许 null |
可以添加空值 |
| 支持下标访问 | 可以通过 get(index) 获取指定位置元素 |
| 自动扩容 | 底层数组空间不足时会自动扩大 |
| 非线程安全 | 多线程同时修改时需要额外处理 |
示例:
java
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Java");
list.add(null);
System.out.println(list);
输出结果:
text
[Java, Java, null]
1.3 ArrayList 与普通数组的区别
普通数组创建后,长度不能改变:
java
String[] names = new String[3];
names[0] = "张三";
names[1] = "李四";
names[2] = "王五";
// names[3] = "赵六"; // 数组越界
而 ArrayList 可以继续添加元素:
java
List<String> names = new ArrayList<>();
names.add("张三");
names.add("李四");
names.add("王五");
names.add("赵六");
System.out.println(names);
输出结果:
text
[张三, 李四, 王五, 赵六]
可以这样理解:
- 普通数组:固定座位的大巴车,座位满了就不能继续上人。
ArrayList:可以增加车厢的列车,座位满了就扩建空间。
1.4 ArrayList 的适用场景
ArrayList 适合以下业务场景:
| 场景 | 原因 |
|---|---|
| 保存查询结果 | 通常按照顺序遍历和展示 |
| 保存商品列表 | 读取和遍历操作较多 |
| 保存文章评论 | 需要按顺序显示 |
| 保存下拉框选项 | 数据量较小,读取方便 |
| 保存接口返回数据 | 便于遍历、过滤和转换 |
例如:
java
List<String> courses = new ArrayList<>();
courses.add("Java");
courses.add("MySQL");
courses.add("Spring Boot");
for (String course : courses) {
System.out.println(course);
}
如果业务中经常需要在列表头部插入或删除大量数据,ArrayList 就不一定合适,因为它需要频繁移动后续元素。
二、创建方式与底层结构
2.1 ArrayList 的创建方式
创建 ArrayList 时,通常推荐使用接口接收实现类:
java
List<String> names = new ArrayList<>();
而不是:
java
ArrayList<String> names = new ArrayList<>();
这样写的好处是:后续如果需要更换集合实现,业务代码修改更少。
java
List<String> names = new ArrayList<>();
// 后续如果确实需要,也可以替换为:
// List<String> names = new LinkedList<>();
ArrayList 常见的创建方式有三种。
1. 无参构造
java
List<String> list = new ArrayList<>();
适合暂时无法确定元素数量的场景。
2. 指定初始容量
java
List<String> list = new ArrayList<>(100);
适合提前知道大概数据量的场景。
例如,预计需要保存 1000 条商品数据:
java
List<String> products = new ArrayList<>(1000);
提前指定容量,可以减少扩容和数组复制的次数。
3. 根据已有集合创建
java
List<String> oldList = Arrays.asList("Java", "MySQL", "Redis");
List<String> newList = new ArrayList<>(oldList);
newList.add("Spring Boot");
System.out.println(newList);
输出结果:
text
[Java, MySQL, Redis, Spring Boot]
2.2 ArrayList 的核心成员变量
以 JDK 8 的源码思想为例,ArrayList 中最重要的成员变量可以简化理解为:
java
public class ArrayList<E> {
// 默认容量
private static final int DEFAULT_CAPACITY = 10;
// 保存元素的底层数组
transient Object[] elementData;
// 当前实际元素数量
private int size;
}
其中最重要的是两个变量:
java
Object[] elementData;
int size;
例如:
java
List<String> list = new ArrayList<>();
list.add("Java");
list.add("MySQL");
list.add("Redis");
底层可以理解为:
text
elementData = ["Java", "MySQL", "Redis", null, null, null, null, null, null, null]
size = 3
2.3 size 与 capacity 的区别
size 和 capacity 是两个非常容易混淆的概念。
| 概念 | 含义 |
|---|---|
size |
当前实际保存了多少个元素 |
capacity |
底层数组目前最多能够容纳多少个元素 |
例如:
text
底层数组容量 capacity = 10
下标: 0 1 2 3 4 5 ...
元素: Java MySQL Redis null null null ...
实际元素数量 size = 3
可以把它理解为一间教室:
- 教室一共有 10 个座位:capacity = 10
- 当前坐了 3 名学生:size = 3
2.4 为什么底层是 Object 数组
ArrayList 可以保存不同类型的数据:
java
List<String> names = new ArrayList<>();
List<Integer> scores = new ArrayList<>();
List<User> users = new ArrayList<>();
但是在底层,ArrayList 使用的是:
java
Object[] elementData;
泛型主要在编译阶段负责限制元素类型。
java
List<String> list = new ArrayList<>();
list.add("Java");
// list.add(100); // 编译错误
2.5 ArrayList 保存的是对象引用
假设有一个用户对象:
java
User user = new User("张三");
List<User> users = new ArrayList<>();
users.add(user);
user.setName("李四");
System.out.println(users.get(0).getName());
输出结果:
text
李四
原因是,ArrayList 保存的是对象引用,而不是对象副本。
text
user ───────┐
▼
User对象{name="李四"}
▲
users[0] ────┘
变量 user 与 users.get(0) 指向的是同一个对象,因此修改对象属性后,从列表中读取到的内容也会发生变化。
三、添加元素与扩容机制
3.1 add(E e) 添加元素
最常见的添加方式如下:
java
List<String> list = new ArrayList<>();
list.add("Java");
list.add("MySQL");
list.add("Redis");
添加元素的核心流程可以概括为:
text
调用 add(element)
↓
判断底层数组是否还有空间
↓
有空间:直接将元素保存到数组末尾
↓
没有空间:创建更大的数组,并复制原有元素
↓
保存新元素,size 加 1
可以简化理解为:
java
elementData[size] = element;
size++;
当然,在真正写入元素之前,ArrayList 会先检查容量是否足够。
3.2 无参构造与懒初始化
执行下面代码时:
java
List<String> list = new ArrayList<>();
列表虽然已经创建,但底层数组此时通常还是空数组。
text
刚创建时:
size = 0
底层数组长度 = 0
当第一次添加元素时:
java
list.add("Java");
底层数组才真正扩展为默认容量 10。
text
第一次添加后:
size = 1
capacity = 10
数组状态可以理解为:
text
["Java", null, null, null, null, null, null, null, null, null]
这是一种懒初始化思想:
text
仓库刚注册时,不急着立即摆满货架;
等第一批货物真正到达时,再准备默认数量的货架。
3.3 第 11 个元素为什么会触发扩容
假设使用无参构造创建列表,并不断添加元素:
java
List<Integer> list = new ArrayList<>();
for (int i = 1; i <= 11; i++) {
list.add(i);
}
容量变化过程可以理解为:
text
添加第 1 个元素:容量扩展为 10
添加第 2 ~ 10 个元素:容量仍然是 10
添加第 11 个元素:原数组放不下,触发扩容
以 JDK 8 的源码为例,扩容核心计算方式如下:
java
int newCapacity = oldCapacity + (oldCapacity >> 1);
其中:
java
oldCapacity >> 1
对于正整数容量,可以简单理解为:
java
oldCapacity / 2
因此:
java
newCapacity = oldCapacity + oldCapacity / 2;
当旧容量为 10 时:
java
newCapacity = 10 + 10 / 2 = 15;
容量变化大致如下:
text
0 → 10 → 15 → 22 → 33 → 49 ...
3.4 为什么不能每次只扩大一个位置
假设每添加一个元素,只增加一个数组位置:
text
添加第 1 个元素:创建长度为 1 的数组
添加第 2 个元素:创建长度为 2 的数组,复制 1 个元素
添加第 3 个元素:创建长度为 3 的数组,复制 2 个元素
添加第 4 个元素:创建长度为 4 的数组,复制 3 个元素
...
这样会导致频繁创建新数组和复制旧数据,性能很差。
按比例扩容则能够减少复制次数:
text
原容量不足
↓
一次多申请一部分空间
↓
后续多次添加可以直接使用空余位置
这是一种时间与空间之间的平衡:
| 扩容方式 | 优点 | 缺点 |
|---|---|---|
| 每次只增加一个位置 | 空间浪费少 | 扩容和复制过于频繁 |
| 一次扩大很多空间 | 添加效率高 | 可能浪费较多空间 |
| 按比例扩容 | 时间与空间较均衡 | 会预留部分空闲位置 |
3.5 Arrays.copyOf() 的作用
数组本身不能直接改变长度。
当旧数组空间不足时,ArrayList 会创建一个更大的新数组,然后将旧元素复制过去。
可以通过下面的代码理解:
java
Integer[] oldArray = {1, 2, 3};
Integer[] newArray = Arrays.copyOf(oldArray, 5);
System.out.println(Arrays.toString(newArray));
输出结果:
text
[1, 2, 3, null, null]
ArrayList 扩容的本质可以简化为:
java
elementData = Arrays.copyOf(elementData, newCapacity);
可以把这个过程理解为搬仓库:
text
旧仓库已经装满
↓
新建一个更大的仓库
↓
把旧货物整体搬到新仓库
↓
继续保存新货物
3.6 ensureCapacity() 与 trimToSize()
如果已经知道大概需要保存多少数据,可以提前预留容量:
java
ArrayList<String> list = new ArrayList<>();
list.ensureCapacity(1000);
for (int i = 0; i < 1000; i++) {
list.add("商品-" + i);
}
这种方式适合:
- 批量读取数据库数据;
- 解析大量文件内容;
- 批量组装接口返回结果;
- 构建大量临时对象列表。
如果列表曾经保存过大量数据,后来删除了很多元素,底层数组容量不会自动缩小。
java
ArrayList<String> list = new ArrayList<>(1000);
list.add("Java");
list.add("MySQL");
list.trimToSize();
调用 trimToSize() 后,底层数组容量会缩减到当前 size。
不过,不建议频繁调用该方法。因为如果后续又继续添加元素,列表可能马上再次扩容。
四、增删改查与元素移动
4.1 get() 与 set():下标访问为什么快
根据下标读取元素:
java
List<String> list = new ArrayList<>();
list.add("Java");
list.add("MySQL");
list.add("Redis");
String value = list.get(1);
System.out.println(value);
输出结果:
text
MySQL
底层可以直接根据下标访问数组:
java
elementData[index]
因此,get(index) 的时间复杂度通常为:
text
O(1)
可以把数组理解为带编号的储物柜:
text
0 号柜:Java
1 号柜:MySQL
2 号柜:Redis
想读取 1 号柜的数据,直接找到对应柜子即可,不需要从头开始寻找。
修改元素同样简单:
java
List<String> list = new ArrayList<>();
list.add("Java");
list.add("MySQL");
list.add("Redis");
String oldValue = list.set(1, "Spring Boot");
System.out.println("旧值:" + oldValue);
System.out.println("新列表:" + list);
输出结果:
text
旧值:MySQL
新列表:[Java, Spring Boot, Redis]
set() 的核心逻辑可以理解为:
java
E oldValue = elementData[index];
elementData[index] = element;
return oldValue;
需要注意:
text
set() 只是替换元素,不会改变列表长度。
4.2 indexOf()、lastIndexOf() 与 contains()
根据元素内容查找位置:
java
List<String> list = new ArrayList<>();
list.add("Java");
list.add("MySQL");
list.add("Java");
list.add("Redis");
System.out.println(list.indexOf("Java"));
System.out.println(list.lastIndexOf("Java"));
System.out.println(list.contains("Redis"));
输出结果:
text
0
2
true
几个方法的作用如下:
| 方法 | 作用 |
|---|---|
indexOf(value) |
从前往后查找第一次出现的位置 |
lastIndexOf(value) |
从后往前查找最后一次出现的位置 |
contains(value) |
判断列表中是否存在指定元素 |
虽然 get(index) 很快,但 contains() 和 indexOf() 通常需要从头比较元素。
例如:
text
查找 Redis:
Java → MySQL → Java → Redis
因此,根据值查找元素的时间复杂度通常为:
text
O(n)
4.3 add(index, element):指定位置插入元素
除了在列表末尾添加元素,还可以在指定位置插入元素:
java
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Redis");
list.add(1, "MySQL");
System.out.println(list);
输出结果:
text
[Java, MySQL, Redis]
执行过程如下:
text
插入前:
下标: 0 1
元素: Java Redis
在下标 1 插入 MySQL:
第一步:Redis 向后移动
下标: 0 1 2
元素: Java Redis Redis
第二步:在下标 1 放入 MySQL
下标: 0 1 2
元素: Java MySQL Redis
底层移动元素时,会使用类似下面的数组复制操作:
java
System.arraycopy(
elementData,
index,
elementData,
index + 1,
size - index
);
其含义是:
text
从插入位置开始,
把后面的元素整体向右移动一格,
为新元素腾出位置。
因此:
| 操作位置 | 移动成本 |
|---|---|
| 尾部添加 | 通常不需要移动元素 |
| 中间插入 | 需要移动一部分元素 |
| 头部插入 | 可能需要移动全部已有元素 |
如果列表中有 10000 个元素:
java
list.add(0, "Java");
可能需要将原有 10000 个元素全部向后移动。
4.4 remove():删除元素为什么可能较慢
根据下标删除元素:
java
List<String> list = new ArrayList<>();
list.add("Java");
list.add("MySQL");
list.add("Redis");
String removed = list.remove(1);
System.out.println("删除元素:" + removed);
System.out.println("删除后:" + list);
输出结果:
text
删除元素:MySQL
删除后:[Java, Redis]
删除过程可以理解为:
text
删除前:
[Java, MySQL, Redis]
删除下标 1 的 MySQL:
Redis 向前移动:
[Java, Redis, Redis]
清空最后一个无效位置:
[Java, Redis, null]
删除完成后,将末尾无效位置设置为 null,可以解除集合对原对象的引用,便于垃圾回收。
根据值删除元素:
java
List<String> list = new ArrayList<>();
list.add("Java");
list.add("MySQL");
list.add("Java");
boolean result = list.remove("Java");
System.out.println(result);
System.out.println(list);
输出结果:
text
true
[MySQL, Java]
需要注意:
text
remove(Object o) 只会删除第一次出现的目标元素。
根据值删除通常包含两个过程:
text
先查找目标元素
↓
再移动后续元素
因此,时间复杂度通常为:
text
O(n)
4.5 Integer 列表中的 remove() 陷阱
这是使用 ArrayList<Integer> 时非常常见的错误。
java
List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.remove(1);
System.out.println(numbers);
输出结果:
text
[10, 30]
这里删除的不是数字 1,而是:
text
下标为 1 的元素,也就是 20。
原因是 ArrayList 中存在两个重载方法:
java
remove(int index)
remove(Object o)
当传入普通整数时:
java
numbers.remove(1);
Java 会优先将其理解为:
java
删除下标为 1 的元素
如果希望删除数值 20,应该写成:
java
numbers.remove(Integer.valueOf(20));
完整示例:
java
List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.remove(Integer.valueOf(20));
System.out.println(numbers);
输出结果:
text
[10, 30]
五、遍历、排序与安全删除
5.1 ArrayList 的常见遍历方式
1. 普通 for 循环
java
List<String> list = Arrays.asList("Java", "MySQL", "Redis");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
适合需要使用下标的场景。
2. 增强 for 循环
java
List<String> list = Arrays.asList("Java", "MySQL", "Redis");
for (String item : list) {
System.out.println(item);
}
适合只读取元素、不需要操作下标的场景。
3. Iterator 迭代器
java
List<String> list = new ArrayList<>();
list.add("Java");
list.add("MySQL");
list.add("Redis");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
System.out.println(item);
}
适合遍历过程中需要安全删除元素的场景。
5.2 遍历过程中直接删除的问题
下面代码可能会抛出 ConcurrentModificationException:
java
List<String> list = new ArrayList<>();
list.add("Java");
list.add("MySQL");
list.add("Redis");
for (String item : list) {
if ("MySQL".equals(item)) {
list.remove(item);
}
}
增强 for 循环底层实际依赖迭代器。
可以把迭代器理解为一个拿着名单点名的人:
text
迭代器已经拿到原始名单
↓
遍历过程中,列表突然被外部修改
↓
迭代器发现名单与实际数据不一致
↓
抛出 ConcurrentModificationException
这种机制称为:
text
fail-fast:快速失败机制
它的作用不是保证线程安全,而是尽快发现遍历过程中不合理的结构性修改。
5.3 正确删除方式
1. 使用 Iterator.remove()
java
List<String> list = new ArrayList<>();
list.add("Java");
list.add("MySQL");
list.add("Redis");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if ("MySQL".equals(item)) {
iterator.remove();
}
}
System.out.println(list);
输出结果:
text
[Java, Redis]
2. 使用 removeIf()
java
List<String> list = new ArrayList<>();
list.add("Java");
list.add("MySQL");
list.add("Redis");
list.add("MyBatis");
list.removeIf(item -> item.startsWith("My"));
System.out.println(list);
输出结果:
text
[Java, Redis]
当删除规则较简单时,removeIf() 更加简洁。
5.4 排序与二分查找
对数字列表进行排序:
java
List<Integer> numbers = new ArrayList<>();
numbers.add(30);
numbers.add(10);
numbers.add(20);
Collections.sort(numbers);
System.out.println(numbers);
输出结果:
text
[10, 20, 30]
对于自定义对象,可以根据指定字段排序:
java
class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return name + "-" + age;
}
}
List<User> users = new ArrayList<>();
users.add(new User("张三", 22));
users.add(new User("李四", 18));
users.add(new User("王五", 25));
users.sort(Comparator.comparingInt(User::getAge));
System.out.println(users);
输出结果:
text
[李四-18, 张三-22, 王五-25]
当列表已经有序时,可以使用二分查找:
java
List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.add(40);
numbers.add(50);
int index = Collections.binarySearch(numbers, 30);
System.out.println(index);
输出结果:
text
2
需要注意:
text
二分查找的前提是:列表必须已经按照相同规则排序。
错误示例:
java
List<Integer> numbers = Arrays.asList(30, 10, 20);
int index = Collections.binarySearch(numbers, 20);
由于列表没有排序,查找结果没有可靠意义。
六、常见陷阱与使用选择
6.1 Arrays.asList() 不能直接增删元素
下面代码会抛出异常:
java
List<String> list = Arrays.asList("Java", "MySQL", "Redis");
list.add("Spring Boot");
原因是:
text
Arrays.asList() 返回的是基于原数组的固定长度列表,
支持修改已有位置,但不支持改变元素数量。
下面的代码是可以执行的:
java
List<String> list = Arrays.asList("Java", "MySQL", "Redis");
list.set(0, "Spring Boot");
System.out.println(list);
输出结果:
text
[Spring Boot, MySQL, Redis]
如果希望继续进行添加和删除操作,可以重新包装成 ArrayList:
java
List<String> list = new ArrayList<>(
Arrays.asList("Java", "MySQL", "Redis")
);
list.add("Spring Boot");
System.out.println(list);
输出结果:
text
[Java, MySQL, Redis, Spring Boot]
6.2 subList() 返回的是视图
subList() 返回的不是完全独立的新列表,而是原列表的一段视图。
java
List<String> list = new ArrayList<>();
list.add("Java");
list.add("MySQL");
list.add("Redis");
list.add("Spring Boot");
List<String> subList = list.subList(1, 3);
subList.set(0, "MongoDB");
System.out.println(list);
输出结果:
text
[Java, MongoDB, Redis, Spring Boot]
原因是:
text
subList 与原列表共享对应范围的数据。
如果希望得到独立副本,应该写成:
java
List<String> copy = new ArrayList<>(list.subList(1, 3));
6.3 clone() 只是浅拷贝
ArrayList 的 clone() 会复制列表容器,但不会复制其中的对象。
java
ArrayList<User> users = new ArrayList<>();
users.add(new User("张三", 20));
ArrayList<User> copy = (ArrayList<User>) users.clone();
copy.get(0).setName("李四");
System.out.println(users.get(0).getName());
输出结果:
text
李四
可以理解为:
text
原列表和复制后的列表是两个盒子,
但两个盒子中保存的是同一个对象地址。
6.4 ArrayList 不是线程安全集合
java
List<Integer> list = new ArrayList<>();
如果多个线程同时修改同一个 ArrayList,可能导致数据错误。
简单场景下,可以使用同步包装:
java
List<String> list = Collections.synchronizedList(new ArrayList<>());
list.add("Java");
list.add("MySQL");
遍历时仍然需要手动同步:
java
synchronized (list) {
for (String item : list) {
System.out.println(item);
}
}
如果业务属于读多写少的场景,例如监听器列表、配置快照列表,可以考虑:
java
List<String> listeners = new CopyOnWriteArrayList<>();
listeners.add("Listener-A");
listeners.add("Listener-B");
不过,CopyOnWriteArrayList 每次修改都可能复制数组,因此不适合高频写入场景。
6.5 ArrayList 与 LinkedList 的选择
ArrayList 与 LinkedList 的简单对比如下:
| 对比项 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组 | 双向链表 |
| 根据下标查询 | 快 | 较慢 |
| 尾部添加 | 快 | 快 |
| 中间插入删除 | 需要移动元素 | 找到节点后修改链接 |
| 内存占用 | 相对较小 | 需要保存前后节点引用 |
| 普通业务使用频率 | 更高 | 特殊场景使用 |
大多数普通业务场景,可以优先使用:
java
List<String> list = new ArrayList<>();
例如:
- 查询结果列表;
- 商品集合;
- 页面展示数据;
- 用户权限集合;
- 配置项列表;
- 临时计算结果。
需要注意的是,中间插入和删除虽然从结构上看链表更方便,但 LinkedList 在定位节点时也需要遍历。实际开发中,不应仅凭"插入删除多"就直接选择 LinkedList,而应该结合数据量、访问方式和性能测试进行判断。
如果业务需求是频繁从头部添加或删除元素,例如队列操作:
java
addLast()
removeFirst()
通常更适合考虑:
java
Deque<String> queue = new ArrayDeque<>();
6.6 常见操作时间复杂度
| 操作 | 示例 | 时间复杂度 | 原因 |
|---|---|---|---|
| 根据下标查询 | get(index) |
O(1) |
数组可以直接定位 |
| 根据下标修改 | set(index, value) |
O(1) |
直接覆盖对应位置 |
| 尾部添加 | add(value) |
摊还 O(1) |
大多数时候直接写入,偶尔扩容 |
| 指定位置插入 | add(index, value) |
O(n) |
需要移动后续元素 |
| 删除末尾元素 | remove(size - 1) |
O(1) |
通常不需要移动 |
| 删除中间元素 | remove(index) |
O(n) |
后续元素需要前移 |
| 根据值删除 | remove(value) |
O(n) |
先查找,再移动 |
| 根据值查找 | contains() / indexOf() |
O(n) |
需要逐个比较 |
| 排序 | sort() |
通常 O(n log n) |
依赖排序算法 |
| 二分查找 | binarySearch() |
O(log n) |
前提是列表已经有序 |
尾部添加的复杂度为什么叫做"摊还 O(1)"?
因为并不是每一次添加都一定只执行一步:
- 前 10 次添加:大多数情况下直接写入数组
- 第 11 次添加:容量不足,需要扩容和复制
- 扩容之后:后续若干次添加又可以直接写入
因此,将大量添加操作整体平均后,尾部添加的平均成本仍然很低。
七、完整测试代码
7.1 测试内容说明
下面的测试代码用于验证以下知识点:
| 测试方法 | 验证内容 |
|---|---|
testBasicOperations() |
基本增删改查 |
testNullAndDuplicate() |
重复元素和 null |
testCapacityGrowth() |
默认容量与扩容过程 |
testEnsureCapacityAndTrim() |
容量预留与压缩 |
testInsertAndRemove() |
中间插入与删除 |
testIntegerRemovePitfall() |
Integer 删除陷阱 |
testSearchSortAndBinarySearch() |
查找、排序、二分查找 |
testIteratorRemove() |
迭代器安全删除 |
testFailFast() |
快速失败机制 |
testSubListView() |
subList() 视图特点 |
testObjectReference() |
对象引用特点 |
7.2 完整可运行代码
java
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;
public class ArrayListLearningDemo {
public static void main(String[] args) throws Exception {
System.out.println("========== ArrayList 学习测试开始 ==========");
testBasicOperations();
testNullAndDuplicate();
testCapacityGrowth();
testEnsureCapacityAndTrim();
testInsertAndRemove();
testIntegerRemovePitfall();
testSearchSortAndBinarySearch();
testIteratorRemove();
testFailFast();
testSubListView();
testObjectReference();
System.out.println("\n========== 所有测试通过 ==========");
}
/**
* 测试基本的添加、查询、修改和删除操作。
*/
private static void testBasicOperations() {
System.out.println("\n--- 1. 基本增删改查测试 ---");
List<String> skills = new ArrayList<>();
skills.add("Java");
skills.add("MySQL");
skills.add("Redis");
assertEquals(3, skills.size(), "添加三个元素后,size 应为 3");
assertEquals("MySQL", skills.get(1), "下标 1 的元素应为 MySQL");
String oldValue = skills.set(1, "Spring Boot");
assertEquals("MySQL", oldValue, "set() 应返回旧值");
assertEquals("Spring Boot", skills.get(1), "修改后的值不正确");
boolean removed = skills.remove("Redis");
assertTrue(removed, "Redis 应该删除成功");
assertEquals(Arrays.asList("Java", "Spring Boot"), skills, "删除后的内容不正确");
System.out.println("当前列表:" + skills);
}
/**
* 测试重复元素和 null。
*/
private static void testNullAndDuplicate() {
System.out.println("\n--- 2. null 与重复元素测试 ---");
List<String> list = new ArrayList<>();
list.add("Java");
list.add(null);
list.add("Java");
list.add(null);
assertEquals(4, list.size(), "列表长度不正确");
assertEquals(0, list.indexOf("Java"), "第一个 Java 的位置不正确");
assertEquals(2, list.lastIndexOf("Java"), "最后一个 Java 的位置不正确");
assertEquals(1, list.indexOf(null), "null 的位置不正确");
System.out.println("当前列表:" + list);
}
/**
* 测试无参构造后的懒初始化和扩容过程。
*
* 需要通过反射观察底层数组长度。
* JDK 9 及以上版本运行时需要添加:
* --add-opens java.base/java.util=ALL-UNNAMED
*/
private static void testCapacityGrowth() throws Exception {
System.out.println("\n--- 3. 扩容机制测试 ---");
ArrayList<Integer> list = new ArrayList<>();
System.out.println("刚创建时:size = " + list.size()
+ ", capacity = " + getCapacity(list));
assertEquals(0, list.size(), "刚创建时 size 应为 0");
assertEquals(0, getCapacity(list), "刚创建时底层容量应为 0");
for (int i = 1; i <= 11; i++) {
list.add(i);
System.out.println("添加第 " + i + " 个元素后:size = "
+ list.size() + ", capacity = " + getCapacity(list));
}
assertEquals(11, list.size(), "元素数量不正确");
assertTrue(getCapacity(list) >= 11, "容量必须能够容纳全部元素");
}
/**
* 测试提前预留容量和缩减容量。
*/
private static void testEnsureCapacityAndTrim() throws Exception {
System.out.println("\n--- 4. ensureCapacity 与 trimToSize 测试 ---");
ArrayList<String> list = new ArrayList<>();
list.ensureCapacity(100);
int capacityAfterEnsure = getCapacity(list);
assertTrue(capacityAfterEnsure >= 100,
"ensureCapacity(100) 后容量应至少为 100");
list.add("Java");
list.add("MySQL");
list.add("Redis");
list.trimToSize();
int capacityAfterTrim = getCapacity(list);
assertEquals(3, capacityAfterTrim,
"trimToSize() 后容量应等于当前元素数量");
System.out.println("预留容量后 capacity = " + capacityAfterEnsure);
System.out.println("压缩容量后 capacity = " + capacityAfterTrim);
}
/**
* 测试指定位置插入和删除元素。
*/
private static void testInsertAndRemove() {
System.out.println("\n--- 5. 指定位置插入与删除测试 ---");
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Redis");
list.add(1, "MySQL");
assertEquals(
Arrays.asList("Java", "MySQL", "Redis"),
list,
"指定位置插入结果不正确"
);
String removed = list.remove(1);
assertEquals("MySQL", removed, "删除的元素不正确");
assertEquals(
Arrays.asList("Java", "Redis"),
list,
"删除中间元素后的结果不正确"
);
System.out.println("最终列表:" + list);
}
/**
* 测试 Integer 列表中 remove() 的重载陷阱。
*/
private static void testIntegerRemovePitfall() {
System.out.println("\n--- 6. Integer remove 重载陷阱测试 ---");
List<Integer> numbers = new ArrayList<>(
Arrays.asList(10, 20, 30, 20)
);
Integer removedByIndex = numbers.remove(1);
assertEquals(Integer.valueOf(20), removedByIndex,
"remove(1) 应删除下标为 1 的元素");
assertEquals(
Arrays.asList(10, 30, 20),
numbers,
"按下标删除后的列表不正确"
);
boolean removedByValue = numbers.remove(Integer.valueOf(20));
assertTrue(removedByValue, "按值删除应该成功");
assertEquals(
Arrays.asList(10, 30),
numbers,
"按值删除后的列表不正确"
);
System.out.println("最终列表:" + numbers);
}
/**
* 测试查找、排序、自定义对象排序和二分查找。
*/
private static void testSearchSortAndBinarySearch() {
System.out.println("\n--- 7. 查找、排序与二分查找测试 ---");
List<Integer> numbers = new ArrayList<>(
Arrays.asList(30, 10, 20, 30, 40)
);
assertEquals(0, numbers.indexOf(30), "第一个 30 的位置不正确");
assertEquals(3, numbers.lastIndexOf(30), "最后一个 30 的位置不正确");
assertTrue(numbers.contains(20), "列表中应该包含 20");
Collections.sort(numbers);
assertEquals(
Arrays.asList(10, 20, 30, 30, 40),
numbers,
"数字排序结果不正确"
);
int index = Collections.binarySearch(numbers, 20);
assertEquals(1, index, "二分查找结果不正确");
List<User> users = new ArrayList<>();
users.add(new User("张三", 22));
users.add(new User("李四", 18));
users.add(new User("王五", 25));
users.sort(Comparator.comparingInt(User::getAge));
assertEquals("李四", users.get(0).getName(), "年龄排序结果不正确");
System.out.println("排序后的数字列表:" + numbers);
System.out.println("20 的位置:" + index);
System.out.println("按照年龄排序后的用户列表:" + users);
}
/**
* 测试使用 Iterator.remove() 安全删除元素。
*/
private static void testIteratorRemove() {
System.out.println("\n--- 8. Iterator 安全删除测试 ---");
List<String> list = new ArrayList<>(
Arrays.asList("Java", "MySQL", "Redis", "MyBatis")
);
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (item.startsWith("My")) {
iterator.remove();
}
}
assertEquals(
Arrays.asList("Java", "Redis"),
list,
"Iterator 删除后的结果不正确"
);
System.out.println("删除后列表:" + list);
}
/**
* 测试遍历过程中直接修改列表导致的快速失败。
*/
private static void testFailFast() {
System.out.println("\n--- 9. fail-fast 测试 ---");
List<String> list = new ArrayList<>(
Arrays.asList("Java", "MySQL", "Redis")
);
boolean exceptionThrown = false;
try {
for (String item : list) {
if ("Java".equals(item)) {
list.remove(item);
}
}
} catch (ConcurrentModificationException e) {
exceptionThrown = true;
System.out.println("捕获到预期异常:" + e.getClass().getSimpleName());
}
assertTrue(exceptionThrown,
"遍历过程中直接删除元素,应该触发快速失败异常");
}
/**
* 测试 subList() 返回的是原列表视图。
*/
private static void testSubListView() {
System.out.println("\n--- 10. subList 视图测试 ---");
List<String> list = new ArrayList<>(
Arrays.asList("Java", "MySQL", "Redis", "Spring Boot")
);
List<String> subList = list.subList(1, 3);
subList.set(0, "MongoDB");
assertEquals("MongoDB", list.get(1),
"修改 subList 后,原列表对应位置也应该发生变化");
List<String> independentCopy = new ArrayList<>(subList);
independentCopy.set(0, "Oracle");
assertEquals("MongoDB", list.get(1),
"修改独立副本后,不应该影响原列表");
System.out.println("原列表:" + list);
System.out.println("独立副本:" + independentCopy);
}
/**
* 测试 ArrayList 中保存的是对象引用。
*/
private static void testObjectReference() {
System.out.println("\n--- 11. 对象引用测试 ---");
User user = new User("张三", 20);
List<User> users = new ArrayList<>();
users.add(user);
user.setName("李四");
assertEquals("李四", users.get(0).getName(),
"修改对象后,列表中读取到的对象属性也应该改变");
System.out.println("列表中的用户:" + users.get(0));
}
/**
* 通过反射读取 ArrayList 底层数组容量。
* 该方法仅用于学习和测试,不应写入实际业务代码。
*/
private static int getCapacity(ArrayList<?> list) throws Exception {
Field field = ArrayList.class.getDeclaredField("elementData");
field.setAccessible(true);
Object[] elementData = (Object[]) field.get(list);
return elementData.length;
}
private static void assertTrue(boolean condition, String message) {
if (!condition) {
throw new AssertionError("断言失败:" + message);
}
}
private static void assertEquals(Object expected, Object actual, String message) {
boolean equals = expected == null
? actual == null
: expected.equals(actual);
if (!equals) {
throw new AssertionError(
"断言失败:" + message
+ ",期望值:" + expected
+ ",实际值:" + actual
);
}
}
/**
* 测试使用的用户类。
*/
private static class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}
}
总结
ArrayList 的本质,是一个基于数组实现的动态列表。
它保留了数组按下标访问速度快的优势,同时通过自动扩容解决了普通数组长度固定的问题。
学习 ArrayList 时,需要重点掌握以下内容:
| 核心知识点 | 结论 |
|---|---|
| 底层结构 | 基于 Object[] 动态数组 |
| 元素特点 | 有序、可重复、允许 null |
| 容量概念 | size 表示元素数量,capacity 表示底层容量 |
| 扩容机制 | 容量不足时创建更大的数组并复制旧元素 |
| 查询修改 | get()、set() 通常为 O(1) |
| 尾部添加 | 摊还时间复杂度为 O(1) |
| 中间插入删除 | 需要移动元素,通常为 O(n) |
| 遍历删除 | 使用 Iterator.remove() 或 removeIf() |
| 线程安全 | ArrayList 本身不是线程安全集合 |
| 常见陷阱 | remove(Integer)、Arrays.asList()、subList() |
在实际开发中,如果业务主要是顺序保存、频繁读取、尾部添加 ,通常可以优先考虑 ArrayList。而当业务需要频繁进行队列式的头尾操作时,可以进一步考虑 ArrayDeque 等更合适的数据结构。