前言
由于长期做业务开发忽略了对 Java 底层的了解和掌握,所以为了巩固基础将会从源码层面,借助 IntelliJ IDEA、通义灵码、Sequence Diagram、Grok 等工具以源码的形式学习和巩固相关基础实现并掌握其设计思路。
所用的 JDK 版本是 1.8.0_462
,环境是Zulu 8.88.0.19-CA-macos-aarch64
。
参考文档主要用的是 Oracle 官方的 8 版本官方文档(English),主要还是以源码为主。如果有参考其他相关文章会在参考中列出。
因作者水平有限,如有理解错误还请指正,谢谢~。
什么是 ArrayList
ArrayList
是一个动态数组(可以理解成一个能自动扩容的数组)。它位于 java.util
包里,底层是用数组实现的,但是普通数组灵活得多,因为它可以随机增加或减少元素数量。
它的特点:
- 动态大小:不像普通数组大小固定,ArrayList 可以根据需要自动扩容或缩容。
- 有序:元素按照添加顺序存储,可以根据索引(索引访问从 0 开始)快速访问。
- 允许重复:可以存多个相同的元素。
- 线程不安全:多线程环境下直接用 ArrayList 会出问题。
- 存对象:ArrayList 只能存对象(比如 Integer、String 等),不能直接存基本数据类型(int、double),但是可以用包装类搞定。
继承/实现关系的说明

继承:让子类继承父类的属性和方法,并可以扩展或重写功能。
实现:接口定义了一组方法规范(行为),类必须实现这些方法,从而保证自己具备接口约定的功能。
Iterable
它是所有可迭代对象的顶层接口,让对象可以通过for-each循环或Iterator遍历元素。Collection<E>
是Java集合框架的顶层接口。它定义了一组操作元素的通用方法,适用于任何集合类型(List、Set、Queue等)。List<E>
定义了一个有序、可索引的集合行为,ArrayList通过实现它,成为一个标准的列表(List)类型。AbstractCollection<E>
是一个抽象类,提供了集合框架的基础功能。AbstractList<E>
是一个抽象类,是AbstractCollection<E>
的子类,提供了列表的通用功能。Cloneable
表示 ArrayList 支持克隆(复制对象)。RandomAccess
这是一个标记接口(没方法),表示ArrayList支持高效的随机访问。Serializable
是java.io
包里的一个接口,没有定义任何方法。它的作用是告诉Java虚拟机(JVM):这个类的对象可以被序列化(转成字节流)和反序列化(从字节流恢复成对象)。
基本使用
csharp
public static void main(String[] args) {
// 创建一个ArrayList
ArrayList<String> list = new ArrayList<>();
// 添加元素
list.add("苹果");
list.add("香蕉");
list.add("橙子");
// 添加已有集合到当前集合末尾
list.addAll(list);
// 删除元素
list.remove("香蕉");
// 获取列表大小
int size = list.size();
// 获取索引位置为0的元素
list.get(0);
// 判断列表中是否包含苹果,返回true
list.contains("苹果");
// 判断列表是否为空,返回false
list.isEmpty();
// list.indexOf("橙子") 获取苹果在列表中的第一个索引位置,返回:1
list.indexOf("橙子");
// 获取橙子在列表中的最后一个索引位置,返回:4
list.lastIndexOf("橙子");
// 获取索引位置为3到列表末尾的子列表,返回:[香蕉, 橙子]
list.subList(3, list.size());
// 将苹果换成草莓
list.set(0, "草莓"); // 将索引为0的元素替换为草莓
// list.set(list.indexOf("苹果"), "草莓");
// 打印列表
System.out.println("当前元素:" + list + ",当前元素大小:" + size);
// 输出: 当前元素:[草莓, 橙子, 苹果, 香蕉, 橙子],当前元素大小:5
}
源码分析
创建对象
在 ArrayList 中创建对象主要有三种方式:无参数的构造方法、通过指定初始容量的构造方法、用现有集合初始化。
无参数的构造方法
ArrayList()
创建一个默认空列表。适用场景:不知道初始大小,动态添加元素。
arduino
/**
* 默认空容量数组,长度为0
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
在创建时,会使用 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
来创建一个空的对象数组。
指定初始容量
ArrayList(int initialCapacity)
创建一个指定容量的空列表。
适用场景:知道大概的一个数据容量,这样数据数据量较大时可以避免的扩容。
⚠️注意:如果初始化长度小于零时会抛出 IllegalArgumentException 的异常。每次扩容是 1.5 倍增长,太小容易频繁扩容,太大空间浪费内存。
java
/*
用于表示空实例的共享空数组实例。通过共享同一个空数组实例来优化内存使用。主要功能:
共享空数组:为所有需要空数组的实例提供一个统一的空数组引用
节省内存:避免每次创建空数组时都分配新的内存空间
初始化用途:通常用作集合类等数据结构的默认初始值
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
// 集合真正存储数据的容器
transient Object[] elementData; // non-private to simplify nested class access
// 在创建时传入一个参数 initialCapacity用于指定长度;
public ArrayList(int initialCapacity) {
// 如果长度大于 0 使用来这个参数来创建一个对象数组;
// 如果为 0 创建一个长度为 0 的空对象数组;
// 如果小于 0 则抛出IllegalArgumentException 异常信息。
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
}
}
从集合创建
ArrayList(Collection c)
用现有集合初始化。适用场景:需要复制或者转换集合,比如从 Set 集合转 List 集合。
⚠️注意:新ArrayList包含传入集合的所有元素,顺序取决于原集合的迭代顺序。传入集合必须是Collection的子类型,且元素类型兼容(? extends E)。
ini
// 记录了ArrayList中实际包含的元素个数,用于跟踪当前存储的元素数量。
private int size;
/*
用于表示空实例的共享空数组实例。通过共享同一个空数组实例来优化内存使用。主要功能:
共享空数组:为所有需要空数组的实例提供一个统一的空数组引用
节省内存:避免每次创建空数组时都分配新的内存空间
初始化用途:通常用作集合类等数据结构的默认初始值
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
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 {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
- 将指定集合 c 中的元素转换为数组a
- 如果数组长度不为 0:
2.1 若 c 是 ArrayList 类型,直接使用该数组,否则复制数组到新的Object数组;
2.2 如果数组长度为 0,使用空数组常量;
使用Arrays.asList()
适用场景:快速用数组初始化 ArrayList 。
csharp
ArrayList<String> list = new ArrayList<>(Arrays.asList("苹果", "香蕉", "橙子"));
list.add("草莓"); // 可以修改
System.out.println(list); // 输出: [苹果, 香蕉, 橙子, 草莓]
注意:
Arrays.asList()
返回一个固定大小的List,不能直接add/remove。new ArrayList<>(Arrays.asList(...))
创建可修改的 ArrayList。
添加元素
往列表末尾加元素

整体实现流程:
- 首先通过
ensureCapacityInternal
确保容量足够,若不足则通过grow()
方法进行扩容; - 然后将元素插入到 elementData 数组的末尾,并更新 size。
- 方法始终返回true。
arduino
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
// 将元素插入到 elementData 数组的末尾,并更新size
elementData[size++] = e;
return true;
}
在指定索引位置插入元素
在指定索引位置插入元素,后面的元素会自动后移

整体实现流程:
首先检查索引是否合法,然后确保容量足够,接着将插入位置及之后的元素右移一位,最后在指定位置插入新元素并增加大小。
scss
public void add(int index, E element) {
// 检查索引是否合法
rangeCheckForAdd(index);
// 检查容量是否充足,不足进行扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 用于将一个数组中的元素复制到另一个数组中,这里是将旧数据复制到新数组中
System.arraycopy(elementData, index, elementData, index + 1, size - index);
// 在指定位置插入新元素
elementData[index] = element;
// 增加实际元素大小
size++;
}
扩容参考:扩容实现
arduino
// 检查索引是否越界
private void rangeCheckForAdd(int index) {
// 指定的索引大于当前的数据长度或者索引小于0,就抛出IndexOutOfBoundsException异常
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
// 索引越界的具体信息
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size;
}
往末尾添加集合
指定集合中的所有元素追加到当前ArrayList的末尾。

整体流程:
首先将集合并入数组,然后确保容量足够,接着通过 System.arraycopy
复制元素,并更新 size。若添加了新元素则返回true,否则 false。
ini
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
扩容参考:扩容实现
修改元素

整体流程:首先检查索引是否越界,然后获取原位置的旧值,将新元素赋值到该位置,并返回旧值。
scss
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
E elementData(int index) {
return (E) elementData[index];
}
删除元素
根据索引位置移除

整体流程:
首先检查索引是否越界,然后获取旧值,接着通过 System.arraycopy
将后续元素前移一位,最后将末尾元素置为 null
以帮助垃圾回收,并返回被删除的元素。
scss
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);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
E elementData(int index) {
return (E) elementData[index];
}
移除指定元素
用于删除列表中首次出现的指定元素。

整体流程:
- 若参数为 null ,则遍历数组查找第一个 null 元素并删除;
- 若参数不为null,则通过equals方法查找匹配元素并删除;
删除成功后返回 true ,否则返回 false。
删除操作由 fastRemove
方法完成,不返回被删元素,直接移动后续元素并置空末尾以助垃圾回收。
arduino
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
/*
首先增加修改计数器modCount,然后计算需要移动的元素个数 numMoved。
如果需要移动元素,则使用System.arraycopy将后面的元素向前复制。
最后将原末尾元素置为null,帮助垃圾回收。
*/
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; // clear to let GC do its work
}
获取元素
csharp
// 创建一个空的ArrayList,默认容量10
ArrayList<String> arrayList = new ArrayList<>();
// 添加元素
arrayList.add("Java");
arrayList.add("Python");
arrayList.add("Go");
// for 循环,性能高,适用于需要通过索引访问和修改元素的需求,需要手工管理索引、注意索引越界的相关问题。
for (int i = 0; i < arrayList.size(); i++) {
System.out.println("item data:" + arrayList.get(i));
}
// for 循环,适用于只读区元素的需求。
for (String item : arrayList) {
System.out.println("item data:" + item);
}
// forEach,依赖于 Iterator,适用于所有实现 Iterable 的集合。适用于读取元素的需求。代码简洁,可读性强。
arrayList.forEach(item -> System.out.println("元素:" + item));
// Iterator 迭代器,适用于所有实现Iterable的集合。适用于在读取元素时控制逻辑(比如边读取边修改和删除元素)。
Iterator<String> iterator = arrayList.iterator();
while (iterator.hasNext()){
// 获取下一个元素
String next = iterator.next();
System.out.println("item data:" + next);
}
案例:使用以上四种方式计算总和
ini
ArrayList<Integer> calcSum = new ArrayList<>();
calcSum.add(10);
calcSum.add(20);
calcSum.add(30);
calcSum.add(40);
// 1. 普通for循环
int sum1 = 0;
for (int i = 0; i < calcSum.size(); i++) {
sum1 += calcSum.get(i);
}
System.out.println("普通for循环总和:" + sum1);
// 2. 增强for循环
int sum2 = 0;
for (Integer item : calcSum) {
sum2 += item;
}
System.out.println("增强for循环总和:" + sum2);
// 3. Iterator迭代器
int sum3 = 0;
Iterator<Integer> iterator = calcSum.iterator();
while (iterator.hasNext()) {
sum3 += iterator.next();
}
System.out.println("Iterator迭代器总和:" + sum3);
// 4. Lambda和Stream
int sum4 = calcSum.stream().mapToInt(Integer::intValue).sum();
System.out.println("Lambda和Stream总和:" + sum4);
扩容机制
观察扩容
ini
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
ArrayList<String> list = new ArrayList<>();
// 存储扩容次数
Set<Integer> set = new HashSet<>(50);
int size = 500;
for (int i = 0; i < size; i++) {
list.add(String.valueOf(i));
// 反射获取elementData数组长度,打印容量
Field elementDataFieid = ArrayList.class.getDeclaredField("elementData");
elementDataFieid.setAccessible(true);
Object[] elementData = (Object[]) elementDataFieid.get(list);
System.out.println("添加第 " + i + " 个元素,当前容量:" + elementData.length);
set.add(elementData.length);
}
System.out.println("扩容次数:" + set.size());
}
/*
输出:
添加第 0 个元素,当前容量:10
添加第 10 个元素,当前容量:15
添加第 15 个元素,当前容量:22
添加第 499 个元素,当前容量:549
添加第 22 个元素,当前容量:33
添加第 33 个元素,当前容量:49
添加第 49 个元素,当前容量:73
添加第 73 个元素,当前容量:109
添加第 109 个元素,当前容量:163
添加第 163 个元素,当前容量:244
添加第 244 个元素,当前容量:366
添加第 366 个元素,当前容量:549
扩容次数:11
*/
扩容实现
说明:由于 ArrayList 源码中多处涉及到了扩容(添加单个【索引、元素】、添加集合【索引、元素】),为了简短文章篇幅,所以将其抽出来进行解释。

arduino
// 调用 ensureCapacityInternal 检查容量是否充足;
private void ensureCapacityInternal(int minCapacity) {
// 调用 calculateCapacity 计算实际所需的容量,如果当前数组是默认空数组,则返回默认容量 10 与所需最小容量中的较大的值;否则直接返回所需最小容量。
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
arduino
// 默认空容量数组,长度为0
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 默认初始大小
private static final int DEFAULT_CAPACITY = 10;
// 计算最小容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
scss
// 检查容量是否充足
private void ensureExplicitCapacity(int minCapacity) {
// 用于检测并发修改。
modCount++;
// 如果当前的数据长度(elementData)小于所需容量(minCapacity),则调用 grow() 方法进行数组扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
arduino
// 数组最大长度:2^31(2147483647) - 8
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
// 获取元素的实际容量长度作为旧容量 oldCapacity
int oldCapacity = elementData.length;
// 计算新容量长度 newCapacity 为原来容量的1.5倍,旧容量长度 + 旧容量长度的一半(通过移位实现)
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果新的容量小于所需最小容量,则使用最小容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 若新容量超过最大数组大小,则调用hugeCapacity处理;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
// 通过Arrays.copyOf将原数组复制到新容量的数组中。
elementData = Arrays.copyOf(elementData, newCapacity);
}
// 处理数组扩容时的长度边界问题
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// 如果新容量超过了数组的最大长度则使用使用最大长度(2^31 -1:2147483647),否则就使用数组的最大长度 - 8(2147483639)
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
问题:
- 为什么是 1.5 倍,不是 2 倍?
- 这其实是一个历史的遗留问题。在云计算时代之前,都是物理机器部署成本比较高,那么如何去优化性能就成了一个必须要考虑的问题,如果每次都扩容到 2 倍,那么在大数据量(比如几十万)的情况下就需要去使用更多的内存空间可能会导致内存溢出,如果这些空间没有被充分使用就会产生空间浪费的问题。如果小于 1.5 倍,那么就需要频繁的去复制数组进行扩容(I/O 的开销会比较大)。经过了一些实践验证认为 1.5 倍会比较合理。
- 其次 2 倍的增长下垃圾回收器 GC 的处理成本也会比较高。
- 为什么要通过移位计算,而不是除法?
- 位移操作是操作的二进制位,CPU 的执行效率比除法计算要高在所有硬件上行为一致,而除法可能因CPU实现略有差异。(ArrayList 从 JDK1.2 版本开始,比较早期);
- 移位操作自动向下取整,与除法运算结果相同;
Integer.MAX_VALUE
和MAX_ARRAY_SIZE
有什么区别?
Integer.MAX_VALUE
是理论上 int 能表示的最大值,分配 Integer.MAX_VALUE 大小的数组(约2GB)可能导致内存溢出,因为JVM需要额外的空间存储元数据。
缩容
缩容定义:缩容是指ArrayList减少底层数组(elementData)的容量,以释放多余的内存空间。
ArrayList
中通过 trimToSize
方法实现 List 的缩容控制,默认情况下不会自动缩容, 哪怕你remove()
很多元素。
如果想实现缩容的处理,可以通过 ArrayList.trimToSize()
来手动缩容到实际的大小。 不会缩容的主要目的在于:
- 避免频繁数组复制操作以此来提高性能,因为缩容的实现是创建一个新的实际大小的数组,然后把数组迁移到新数组上;
- 保证容量大于等于需要存储的容量,为后续添加元素预留空间。
arduino
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size);
}
}
线程安全问题
什么是fail-fast机制?
fail-fast(快速失败)是 Java 集合在遍历时,如果发现有人改了集合的结构(比如添加元素、移除元素)时就会立即抛出 ConcurrentModificationException
异常并停止操作。这就相当于你在算账,突然又有人来消费了,这时你就得被迫终止,因为不论你怎么算账目都是错的。fail-fast 就承担了"监控摄像头"的这个角色。
所以 fail-fast 本质上就是一种保护集合的数据一致性的安全机制,防止在遍历过程中因为意外修改导致程序出错或数据损坏。
与 fail-fast 相反的是 fail-safe
机制来实现遍历修改集合内容时保证遍历过程不会被意外打断。原理是每次修改(add、remove等)时,复制一份底层数组,修改在新副本上,迭代器使用旧数组,以实现快照隔离。
为啥需要这个东西?
- 防止数据不一致问题
ArrayList 的底层是一个数组,遍历时依赖索引或者 Iterator。如果在遍历过程中,列表被修改(比如删除元素、数组元素前移),Iterator 可能访问到错误的位置或索引越界,导致数据发生问题。
- 数据混乱
ArrayList不是线程安全的,多个线程同时操作(一个遍历,一个修改)可能导致数据混乱。如果需要保证线程安全可以使用 CopyOnWriteArrayList
。
vbnet
public static void main(String[] args) {
List<String> failFast = new ArrayList<>();
failFast.add("1");
failFast.add("2");
failFast.add("3");
failFast.add("4");
Iterator<String> iterator = failFast.iterator();
while (iterator.hasNext()){
String next = iterator.next();
if ("2".equals(next)){
// 此时就会触发 ConcurrentModificationException
failFast.remove(next);
}
}
System.out.println(failFast);
}
如何解决?
- 使用 Iterator
vbnet
while (iterator.hasNext()){
String next = iterator.next();
if ("2".equals(next)){
// 安全删除,不会触发fail-fast
iterator.remove();
}
}
System.out.println(failFast); // 输出:[1, 3, 4]
- 使用 CopyOnWriteArrayList
csharp
CopyOnWriteArrayList<String> failFast = new CopyOnWriteArrayList<>();
failFast.add("1");
failFast.add("2");
failFast.add("3");
failFast.add("4");
for (String fruit : failFast) {
if (fruit.equals("2")) {
failFast.remove(fruit);
}
}
System.out.println(failFast); // 输出:[1, 3, 4]
- 收集后再删除
csharp
ArrayList<String> failFast = new ArrayList<>();
failFast.add("1");
failFast.add("2");
failFast.add("3");
failFast.add("4");
// 收集需要删除的元素
ArrayList<String> removeItem = new ArrayList<>();
for (String item : failFast) {
if ("2".equals(item))
removeItem.add(item);
}
// 移除元素集合
failFast.removeAll(removeItem);
System.out.println(failFast); // 输出:[1, 3, 4]
实现原理
- modCount :ArrayList内部的一个int字段,记录集合的结构修改次数(如add、remove、clear等操作会增加modCount)。
- Iterator :ArrayList的迭代器(Iterator 类)在创建时会记录
modCount
的值(存为expectedModCount
),并在每次迭代时检查是否一致。 - 异常触发:如果modCount与expectedModCount不一致,说明集合被修改,抛出ConcurrentModificationException。
csharp
/*
用于记录列表结构被修改的次数。当列表大小改变或发生其他结构性变化时,该计数器递增。
迭代器使用此字段检测并发修改,若发现不一致则抛出ConcurrentModificationException,实现快速失败机制。
*/
protected transient int modCount = 0;
// expectedModCount记录迭代器创建时列表的修改次数
int expectedModCount = modCount;
// ArrayList 的内部类
private class Itr implements Iterator<E> {
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
// 检查数据版本是否被修改
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
/*
这段代码用于实现迭代器的并发修改检测机制:
expectedModCount记录迭代器创建时列表的修改次数.
通过与modCount(列表实际修改次数)比较
如果两者不一致,说明在迭代过程中列表被外部修改,从而检测到并发修改异常,保证迭代过程的数据一致性
*/
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}