ArrayList 是 Java 集合框架中最常用的动态数组实现,位于 java.util 包。它实现了 List 接口,允许存储重复元素和 null 值。与普通数组相比,ArrayList 的大小可以动态调整,无需在创建时指定固定长度。
由于 ArrayList 内部采用数组进行实现,我们可以从数组的两 大核心特性去了解 ArrayList:
数组在内存中是连续存储的空间,每个元素的内存地址可通过 “数组起始地址 + 元素索引 × 元素占用字节数” 直接计算得出(类似 “知道小区门牌号和单元号,能直接找到住户”)。
这种定位方式无需像链表那样逐个节点遍历查找,因此无论数组长度多少,通过索引查询元素的时间复杂度都是 O(1)(即 “常数级速度”),遍历(按索引顺序访问所有元素)时也能高效利用 CPU 缓存(缓存偏好连续内存片段),进一步提升速度。
数组的长度在初始化后通常固定(或需显式扩容),增删元素会打破原有连续结构,需额外操作:
删除数组中间某元素后,其后面所有元素需 “向前移动一位” 以填补空位(避免内存间隙),若数组长度大,移动的元素数量多。若删除后需缩小数组长度,还需拷贝剩余元素到新的小容量数组中。
若数组已满,需先 “扩容”(如 ArrayList 默认扩为原容量 1.5 倍)—— 创建新的大容量数组,将原数组元素全部拷贝过去。即便数组未满,若在中间插入元素,其后面所有元素需 “向后移动一位” 以腾出位置。这些 “移动 / 拷贝” 操作的时间复杂度随数组长度增加而增加,为 O(n)(n 为数组长度),因此增删效率远低于查询。
ArrayList 提供了三种构造方法,用于初始化不同状态的列表,下面分别介绍:
无参构造函数,创建一个初始为空的列表。底层使用 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 空数组,首次添加元素时会自动扩容至 DEFAULT_CAPACITY。定义如下:
public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
创建一个具有指定初始容量的空列表,定义如下:
public ArrayList(int initialCapacity) { if (initialCapacity > 0) { // 初始容量为正数时,创建对应长度的数组 this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { // 初始容量为0时,使用 EMPTY_ELEMENTDATA 空数组 this.elementData = EMPTY_ELEMENTDATA; } else { // 初始容量为负数时,抛出异常 throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); } }
创建一个包含指定集合所有元素的列表,元素顺序与集合的迭代器顺序一致。定义如下:
public ArrayList(Collection<? extends E> c) { // 将集合转换为数组 elementData = c.toArray(); // 如果集合不为空 if ((size = elementData.length) != 0) { // 检查转换后的数组是否为 Object[] 类型,若不是则复制为 Object[] if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // 集合为空时,使用 EMPTY_ELEMENTDATA 空数组 this.elementData = EMPTY_ELEMENTDATA; } }
下面通过代码示例展示 ArrayList 的常用操作:
package com.hxstrive.java_collection.arrayList; import java.util.ArrayList; import java.util.List; public class ArrayListDemo { public static void main(String[] args) { // 1. 创建 ArrayList List<String> fruits = new ArrayList<>(); // 2. 添加元素 fruits.add("Apple"); fruits.add("Banana"); fruits.add("Cherry"); fruits.add(1, "Blueberry"); // 在指定索引插入 System.out.println("添加后: " + fruits); // 输出: [Apple, Blueberry, Banana, Cherry] // 3. 获取元素 String first = fruits.get(0); System.out.println("第一个元素: " + first); // 输出: Apple // 4. 修改元素 fruits.set(2, "Grape"); System.out.println("修改后: " + fruits); // 输出: [Apple, Blueberry, Grape, Cherry] // 5. 删除元素 fruits.remove(3); // 按索引删除 fruits.remove("Blueberry"); // 按元素删除 System.out.println("删除后: " + fruits); // 输出: [Apple, Grape] // 6. 遍历元素 System.out.println("遍历元素:"); for (String fruit : fruits) { System.out.println(fruit); } // 7. 其他常用方法 System.out.println("是否包含 Apple: " + fruits.contains("Apple")); // true System.out.println("元素数量: " + fruits.size()); // 2 System.out.println("是否为空: " + fruits.isEmpty()); // false // 8. 清空列表 fruits.clear(); System.out.println("清空后: " + fruits); // 输出: [] } }
下面将通过分析 ArrayList 的源码了解定义的成员变量,以及它们的用法:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { @java.io.Serial private static final long serialVersionUID = 8683452581122892189L; /** * 默认初始容量,当首次添加元素时,空列表会扩容到此容量 */ private static final int DEFAULT_CAPACITY = 10; /** * 共享的空数组实例,用于构造函数指定初始容量为0时使用 * 与DEFAULTCAPACITY_EMPTY_ELEMENTDATA的区别在于扩容策略不同 */ private static final Object[] EMPTY_ELEMENTDATA = {}; /** * 共享的空数组实例,用于默认构造函数创建的空列表 * 与EMPTY_ELEMENTDATA区分开来,以便在首次添加元素时知道需要扩容到DEFAULT_CAPACITY */ private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /** * 存储ArrayList元素的底层数组缓冲区 * ArrayList的容量就是这个数组的长度 * 对于elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA的空列表, * 当添加第一个元素时会扩容到DEFAULT_CAPACITY * * 使用transient关键字修饰,表明该数组不会被默认序列化机制序列化 * (通过自定义writeObject方法优化序列化性能,只序列化实际元素) * 访问修饰符不是private,是为了简化嵌套类的访问 */ transient Object[] elementData; /** * ArrayList中实际包含的元素数量(不是底层数组的容量) * @serial 表明该字段会被序列化 */ private int size; }
add() 方法用来添加元素到 ArrayList 中,如果容量不够,则会进行扩容操作。下面就通过源码来了解一下:
/** * 将指定元素追加到列表的末尾 * * @param e 要添加到列表中的元素 * @return {@code true}(按照{@link Collection#add}的规范) */ public boolean add(E e) { // 在 AbstractList 中进行定义 // 记录修改次数,用于迭代器的快速失败机制 modCount++; // 调用辅助方法执行实际的添加操作 add(e, elementData, size); return true; }
add(e, elementData, size) 方法定义如下:
/** * 从add(E)方法中拆分出来的辅助方法,用于将方法字节码大小控制在35字节以内 * (-XX:MaxInlineSize 的默认值),这在 add(E) 方法被C1编译的循环调用时有助于性能优化 * * @param e 要添加的元素 * @param elementData 存储元素的底层数组 * @param s 当前列表中的元素数量(即下一个要添加元素的索引位置) */ private void add(E e, Object[] elementData, int s) { // 如果当前元素数量等于数组长度,说明数组已满,需要扩容 if (s == elementData.length) elementData = grow(); // 将元素添加到数组的指定位置(当前元素数量对应的索引) elementData[s] = e; // 元素数量加1 size = s + 1; }
扩容操作:
/** * 扩容方法的简化版本,默认按照当前元素数量+1的需求进行扩容 * @return 扩容后的新数组 */ private Object[] grow() { // 调用带参的 grow 方法,最小容量为当前元素数量+1 return grow(size + 1); } /** * 增加容量以确保列表至少能容纳指定的最小容量的元素 * * @param minCapacity 期望的最小容量 * @throws OutOfMemoryError 如果 minCapacity 为负数(表示容量溢出) */ private Object[] grow(int minCapacity) { // 获取当前底层数组的容量 int oldCapacity = elementData.length; // 处理非默认空数组的情况(包括已初始化容量或用户指定容量为0的情况) if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 计算新容量:使用 ArraysSupport 工具类计算,遵循以下规则: // 1. 最小增长为 (minCapacity - oldCapacity),确保至少满足最小容量需求 // 2. 首选增长为 oldCapacity >> 1(即旧容量的一半),实现 1.5 倍左右的扩容 int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, /* 最小增长量 */ oldCapacity >> 1 /* 首选增长量 */); // 复制旧数组元素到新容量的数组中,并更新底层数组引用 return elementData = Arrays.copyOf(elementData, newCapacity); } else { // 处理默认空数组的情况(使用无参构造创建的列表) // 首次扩容时,容量取默认容量(10)和最小需求容量中的较大值 return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)]; } }
get() 方法用于从 ArrayList 获取一个元素,实现如下:
/** * 返回列表中指定位置的元素 * * @param index 要返回元素的索引位置 * @return 列表中指定索引位置的元素 * @throws IndexOutOfBoundsException 如果索引超出范围(index < 0 或 index >= size()) */ public E get(int index) { // 检查索引的有效性,若索引越界则抛出 IndexOutOfBoundsException Objects.checkIndex(index, size); // 调用 elementData 方法获取指定索引处的元素 return elementData(index); } /** * 从底层数组中获取指定索引处的元素,并进行类型转换 * * @param index 要获取元素的索引位置(已确保在有效范围内) * @return 转换为泛型E类型的指定索引处的元素 */ @SuppressWarnings("unchecked") // 抑制 unchecked 类型转换的警告,因为底层数组存储的是E类型元素 E elementData(int index) { // 从底层数组elementData中获取指定索引的元素,并强制转换为泛型E类型 return (E) elementData[index]; }
/** * 移除列表中指定索引位置的元素 * 后续所有元素会向左移动一位(即它们的索引值减1) * * @param index 要移除元素的索引位置 * @return 从列表中被移除的元素 * @throws IndexOutOfBoundsException 如果索引超出范围(index < 0 或 index >= size()) */ public E remove(int index) { // 1. 检查索引有效性:若索引越界,直接抛出IndexOutOfBoundsException Objects.checkIndex(index, size); // 2. 缓存底层数组引用(避免多次访问成员变量,提升效率) final Object[] es = elementData; // 3. 取出要移除的元素并强制转换为泛型E类型(抑制unchecked警告, // 因底层数组存储的是E类型元素) @SuppressWarnings("unchecked") E oldValue = (E) es[index]; // 4. 调用私有快速移除方法,执行实际的元素移除和数组移位逻辑 fastRemove(es, index); // 5. 返回被移除的元素 return oldValue; } /** * 私有移除方法:跳过索引边界检查(前提是外部调用已确保索引合法),且不返回被移除的元素 * 用于内部高效移除元素,避免重复边界检查和返回值处理的开销 * * @param es 存储元素的底层数组(已通过外部传入,避免重复获取成员变量) * @param i 要移除元素的索引位置(已确保合法,无需二次检查) */ private void fastRemove(Object[] es, int i) { // 1. 增加修改计数器:用于迭代器的快速失败(fail-fast)机制,标记列表结构已变更 modCount++; // 2. 计算移除元素后的新元素数量(原数量-1) final int newSize; // 3. 判断被移除元素是否为列表的最后一个元素: // - 若 newSize > i(即移除的不是最后一个元素),则需要将后续元素向左移位 if ((newSize = size - 1) > i) // 调用系统级数组复制方法:将 es 中从 i+1 位置开始的元素,复制到 es 中从i位置开始的区域 // 复制长度为 (newSize - i),即后续需要移位的元素总数 System.arraycopy(es, i + 1, es, i, newSize - i); // 4. 清空最后一个元素的引用(原size位置,现在已变为newSize),帮助GC回收内存 // 同时更新列表的实际元素数量size为newSize es[size = newSize] = null; }
更多源码请查看 ArrayList 源码。