【Java】ArrayList详解:从使用到源码的全面剖析
作为 Java 集合框架中最常用的类之一,ArrayList 凭借其动态扩容的特性、便捷的增删改查操作,成为日常开发中的 “常客”。但很多开发者可能只停留在 “会用” 的层面,对其底层实现、扩容机制及性能优化却一知半解。今天这篇文章,我将从实战使用、源码解析、性能分析三个维度,带大家彻底吃透 ArrayList!
一、ArrayList 核心特性
ArrayList 是 List 接口的动态数组实现,核心特点如下:
- 基于数组存储元素,支持随机访问(通过索引直接获取元素)
- 元素有序且可重复,允许存储 null 值
- 容量动态扩容,默认初始容量为 10
- 线程不安全,适合单线程环境(多线程可使用
Collections.synchronizedList()或CopyOnWriteArrayList)
二、ArrayList 实战使用
1. 创建 ArrayList
ArrayList 提供三种常见创建方式,根据场景选择:
// 1. 基本创建方式(指定元素类型)
ArrayList<String> alist1 = new ArrayList();
// 2. 基于List接口(类型推断,JDK7+支持)
List<String> alist2 = new ArrayList<>();
// 3. 指定初始容量(避免频繁扩容,推荐已知元素数量时使用)
List<String> alist3 = new ArrayList<>(15);
2. 增删改查操作
(1)添加元素
- 末尾添加:
add(E e) - 指定位置添加:
add(int index, E element)
List<String> userList = new ArrayList<>();
// 末尾添加元素
userList.add("张三");
userList.add("李四");
userList.add(null); // 允许添加null
// 指定索引添加(索引0-2,超出范围抛IndexOutOfBoundsException)
userList.add(1, "王五");
System.out.println(userList); // 输出:[张三, 王五, 李四, null]
(2)修改元素
使用set(int index, E element)通过索引修改元素:
// 将索引0的元素改为"张三丰"
userList.set(0, "张三丰");
System.out.println(userList); // 输出:[张三丰, 王五, 李四, null]
(3)删除元素
- 按索引删除:
remove(int index) - 按元素删除:
remove(Object o)
// 按索引删除(返回被删除元素)
String removed = userList.remove(2);
System.out.println("删除的元素:" + removed); // 输出:李四
// 按元素删除(删除第一个匹配的元素,返回boolean)
boolean isRemoved = userList.remove("王五");
System.out.println("是否删除成功:" + isRemoved); // 输出:true
// 删除null元素
userList.remove(null);
(4)查找元素
- 索引查找:
get(int index) - 元素位置查找:
indexOf(Object o)、lastIndexOf(Object o) - 包含判断:
contains(Object o)
// 通过索引获取元素
String first = userList.get(0);
System.out.println("第一个元素:" + first); // 输出:张三丰
// 查找元素位置(正序)
int index = userList.indexOf("张三丰");
System.out.println("元素位置:" + index); // 输出:0
// 查找元素位置(倒序)
userList.add("张三丰");
int lastIndex = userList.lastIndexOf("张三丰");
System.out.println("最后一次出现的位置:" + lastIndex); // 输出:1
// 判断是否包含元素
boolean hasElement = userList.contains("张三丰");
System.out.println("是否包含该元素:" + hasElement); // 输出:true
3. 排序与二分查找
若元素已排序,可使用二分查找提升查询效率:
List<Integer> numList = new ArrayList<>(Arrays.asList(3, 1, 4, 1, 5, 9));
// 排序(默认升序)
Collections.sort(numList);
System.out.println("排序后:" + numList); // 输出:[1, 1, 3, 4, 5, 9]
// 二分查找(返回元素索引,未找到返回负数)
int searchIndex = Collections.binarySearch(numList, 4);
System.out.println("元素4的位置:" + searchIndex); // 输出:3
三、底层源码解析
1. 核心成员变量
ArrayList 的核心状态由三个变量维护:
// 存储元素的数组(transient避免序列化时存储空元素)
transient Object[] elementData;
// 实际元素个数
private int size;
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
2. 构造方法
// 无参构造:初始化空数组(首次添加元素时扩容为10)
public 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);
}
}
3. 扩容机制(关键!)
当添加元素时,若数组已满(size == elementData.length),会触发扩容。核心逻辑在grow()方法中:
private void grow(int minCapacity) {
// 原容量
int oldCapacity = elementData.length;
// 新容量 = 原容量 + 原容量/2(1.5倍扩容)
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 处理初始为空数组的情况(首次扩容为10)
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 处理超大容量(超过Integer.MAX_VALUE - 8时)
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 复制原数组元素到新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
扩容关键结论:
- 默认扩容为原容量的 1.5 倍(通过右移操作
oldCapacity >> 1实现,效率更高) - 首次添加元素时,空数组会扩容为默认容量 10
- 扩容时会创建新数组并复制元素,频繁扩容会影响性能(建议提前指定初始容量)
4. 关键方法源码
(1)add(int index, E element)
指定位置添加元素需移动后续元素:
public void add(int index, E element) {
// 检查索引合法性
rangeCheckForAdd(index);
// 确保容量足够
ensureCapacityInternal(size + 1);
// 复制数组:index及后续元素向后移动1位(本地方法,效率较高)
System.arraycopy(elementData, index, elementData, index + 1, size - index);
// 插入新元素
elementData[index] = element;
size++;
}
(2)remove(int index)
删除元素需移动后续元素填补空位:
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;
}
四、时间复杂度分析
ArrayList 的性能关键在于 “索引操作” 与 “非索引操作” 的区别:
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 查询(get) | O(1) | 直接通过索引访问,随机访问效率高 |
| 修改(set) | O(1) | 索引定位后直接修改 |
| 末尾添加(add) | O(1) | 无元素移动,仅需赋值 |
| 中间 / 开头添加 | O(n) | 需移动后续 n 个元素 |
| 中间 / 开头删除 | O(n) | 需移动后续 n 个元素 |
性能优化建议:
- 已知元素数量时,创建时指定初始容量,避免扩容
- 频繁在中间插入 / 删除元素时,考虑使用 LinkedList(O (1) 插入删除,但查询 O (n))
- 遍历 ArrayList 时,使用增强 for 循环或迭代器(效率高于普通 for 循环)
五、常见面试题
- ArrayList 与 LinkedList 的区别?
- 底层实现:ArrayList 基于数组,LinkedList 基于双向链表
- 访问效率:ArrayList 随机访问 O (1),LinkedListO (n)
- 插入删除:ArrayList 中间操作 O (n),LinkedListO (1)
- 内存占用:ArrayList 连续存储,LinkedList 需存储节点指针,内存开销更大
- ArrayList 的扩容机制?
- 默认初始容量 10,扩容为原容量的 1.5 倍
- 扩容时通过
Arrays.copyOf()复制元素到新数组 - 首次添加元素时,空数组扩容为 10
- ArrayList 如何保证线程安全?
- 原生线程不安全
- 解决方案:
Collections.synchronizedList(list)、CopyOnWriteArrayList、Vector(不推荐,效率低)
总结
ArrayList 是 Java 开发中最基础也最常用的集合类,掌握其使用方法、底层实现及性能特点,能帮助我们在实际开发中做出更合理的选择。希望这篇文章能让你对 ArrayList 有更深入的理解,后续我还会分享更多 Java 集合框架的源码解析,敬请关注!



