深入浅出 ArrayList:从基础用法到底层原理的全面解析(下)
五、ArrayList 底层核心原理 —— 扩容机制与性能优化
很多开发者使用 ArrayList 时,只知道 “它能自动扩容”,但不清楚扩容的具体逻辑 —— 比如 “什么时候扩容?”“扩容到多大?”“扩容过程消耗性能吗?”。理解这些底层原理,能帮助你写出更高效的代码。
5.1 扩容的触发条件
ArrayList 的扩容是在 “添加元素时” 触发的,核心入口是ensureCapacityInternal(int minCapacity)
方法,minCapacity
表示 “当前需要的最小容量”(即size + 1
,因为要添加 1 个元素)。
扩容的核心逻辑可以概括为:
- 计算当前需要的最小容量
minCapacity
(添加 1 个元素就是size + 1
,添加 n 个元素就是size + n
); - 比较
minCapacity
和当前数组容量(elementData.length
):- 如果
minCapacity <= elementData.length
:容量足够,无需扩容; - 如果
minCapacity > elementData.length
:容量不足,触发扩容。
- 如果
5.2 扩容的具体流程(JDK 1.8)
我们通过源码一步步拆解扩容流程,核心方法包括ensureCapacityInternal()
、ensureExplicitCapacity()
、grow()
。
步骤 1:ensureCapacityInternal (int minCapacity)—— 计算最小容量
private void ensureCapacityInternal(int minCapacity) {// 如果elementData是默认空数组(无参构造初始化的情况),则最小容量取DEFAULT_CAPACITY(10)和minCapacity的最大值if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);}// 检查是否需要显式扩容ensureExplicitCapacity(minCapacity);
}
比如:无参构造初始化后,第一次调用add()
,minCapacity = size + 1 = 1
,此时elementData
是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,所以minCapacity
会被更新为max(10, 1) = 10
。
步骤 2:ensureExplicitCapacity (int minCapacity)—— 判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {modCount++; // 修改计数器(用于迭代器的快速失败机制)// 如果最小容量 > 当前数组容量:需要扩容if (minCapacity - elementData.length > 0) {grow(minCapacity); // 核心扩容方法}
}
modCount
是 “集合修改次数计数器”,每次 add/remove/clear 操作都会让modCount++
,迭代器遍历前会记录expectedModCount = modCount
,如果遍历过程中modCount != expectedModCount
,就会抛出ConcurrentModificationException
(快速失败机制)。
步骤 3:grow (int minCapacity)—— 执行扩容(核心方法)
grow()
是 ArrayList 扩容的核心,负责创建新数组、复制元素、替换底层数组,源码:
private void grow(int minCapacity) {// 获取当前数组容量(oldCapacity)int oldCapacity = elementData.length;// 计算新容量:oldCapacity + (oldCapacity >> 1) → 即oldCapacity * 1.5(右移1位相当于除以2)int newCapacity = oldCapacity + (oldCapacity >> 1);// 如果新容量 < 最小容量:则新容量 = 最小容量(避免扩容后仍不够用)if (newCapacity - minCapacity < 0) {newCapacity = minCapacity;}// 如果新容量 > 最大数组容量(Integer.MAX_VALUE - 8):则新容量取Integer.MAX_VALUE(避免溢出)if (newCapacity - MAX_ARRAY_SIZE > 0) {newCapacity = hugeCapacity(minCapacity);}// 复制原数组元素到新数组(新容量为newCapacity),并替换elementDataelementData = Arrays.copyOf(elementData, newCapacity);
}// 处理超大容量的情况
private static int hugeCapacity(int minCapacity) {if (minCapacity < 0) { // 溢出(minCapacity为负,说明int超出最大值)throw new OutOfMemoryError();}// 如果最小容量 > MAX_ARRAY_SIZE:返回Integer.MAX_VALUE,否则返回MAX_ARRAY_SIZEreturn (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}// 数组的最大容量(Integer.MAX_VALUE - 8):因为某些JVM会在数组末尾保留一些头部信息
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
扩容流程总结:
- 新容量默认是旧容量的 1.5 倍(通过
oldCapacity + (oldCapacity >> 1)
计算,右移操作比乘法更高效); - 如果 1.5 倍后的新容量仍小于 “最小需要容量”(比如一次性添加大量元素),则新容量直接等于 “最小需要容量”;
- 如果新容量超过
MAX_ARRAY_SIZE
(Integer.MAX_VALUE - 8
),则根据 “最小需要容量” 判断:如果minCapacity > MAX_ARRAY_SIZE
,新容量取Integer.MAX_VALUE
,否则取MAX_ARRAY_SIZE
; - 通过
Arrays.copyOf(elementData, newCapacity)
创建新数组,并将原数组元素复制到新数组,最后将elementData
指向新数组,完成扩容。
5.3 扩容的性能消耗
扩容的核心操作是Arrays.copyOf()
,而Arrays.copyOf()
底层调用的是System.arraycopy()
(native 方法,由 C/C++ 实现),虽然效率较高,但 “复制元素” 的过程仍然是 O (n) 时间复杂度 —— 如果频繁扩容(比如每次只添加 1 个元素,容量从 10→15→22→33...),会累积较多的复制操作,影响性能。
因此,优化建议:如果提前知道集合的大致元素数量,尽量用ArrayList(int initialCapacity)
指定初始容量,避免频繁扩容。
示例:存储 10000 个元素,无参构造 vs 指定初始容量的性能对比
public class ArrayListCapacityOptDemo {public static void main(String[] args) {// 无参构造:会经历多次扩容(10→15→22→33→...→10000+)long start1 = System.currentTimeMillis();List<Integer> list1 = new ArrayList<>();for (int i = 0; i < 10000; i++) {list1.add(i);}long end1 = System.currentTimeMillis();System.out.println("无参构造耗时:" + (end1 - start1) + "ms");// 指定初始容量10000:无需扩容long start2 = System.currentTimeMillis();List<Integer> list2 = new ArrayList<>(10000);for (int i = 0; i < 10000; i++) {list2.add(i);}long end2 = System.currentTimeMillis();System.out.println("指定初始容量耗时:" + (end2 - start2) + "ms");}
}
输出结果(仅供参考):
无参构造耗时:3ms
指定初始容量耗时:1ms
可以看到,指定初始容量能显著减少性能消耗,元素越多,优化效果越明显。
5.4 缩容:trimToSize () 方法
ArrayList 只有 “自动扩容”,没有 “自动缩容”—— 比如集合中原本有 100 个元素(数组容量 100),删除 90 个后,元素只剩 10 个,但数组容量仍然是 100,会浪费内存。
此时可以调用trimToSize()
方法 “手动缩容”,将数组容量缩小到与size
(实际元素个数)一致,源码:
public void trimToSize() {modCount++;// 如果数组容量 > 实际元素个数:缩容if (size < elementData.length) {// 如果size=0:用空数组,否则复制到size大小的数组elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size);}
}
示例:
List<String> list = new ArrayList<>(100); // 初始容量100
list.add("a");
list.add("b");
System.out.println("缩容前数组容量:" + getCapacity(list)); // 输出:100(需要反射获取elementData长度)list.trimToSize(); // 缩容
System.out.println("缩容后数组容量:" + getCapacity(list)); // 输出:2// 反射获取ArrayList的elementData长度(即数组容量)
private static int getCapacity(List<?> list) {try {Field field = ArrayList.class.getDeclaredField("elementData");field.setAccessible(true);Object[] elementData = (Object[]) field.get(list);return elementData.length;} catch (Exception e) {e.printStackTrace();return 0;}
}
注意:trimToSize()
适合 “确定后续不会再添加大量元素” 的场景,否则缩容后再添加元素,又会触发扩容,反而增加性能消耗。
六、ArrayList 线程安全问题 ——3 种解决方案
前文提到,ArrayList 是非线程安全的,在多线程环境下进行 “添加 / 删除” 操作会出现异常或数据不一致。下面介绍 3 种常用的线程安全解决方案,以及它们的优缺点对比。
6.1 方案 1:使用 Vector(不推荐)
Vector 是 Java 早期提供的 “线程安全版 ArrayList”,它的所有方法(add/remove/get 等)都用synchronized
修饰,保证了线程安全。
示例:
// Vector的add方法(源码)
public synchronized boolean add(E e) {modCount++;ensureCapacityHelper(elementCount + 1);elementData[elementCount++] = e;return true;
}// 多线程使用Vector
public class VectorThreadSafeDemo {public static void main(String[] args) {List<String> vector = new Vector<>();// 线程1添加元素new Thread(() -> {for (int i = 0; i < 1000; i++) {vector.add("元素" + i);}}).start();// 线程2遍历元素new Thread(() -> {for (String s : vector) {System.out.println(s);}}).start();}
}
优缺点:
- 优点:使用简单,无需额外处理,直接替换 ArrayList 即可;
- 缺点:
synchronized
修饰整个方法,锁粒度大,多线程并发访问时效率低(比如 100 个线程同时 add,需要排队执行),现在已很少使用。
6.2 方案 2:使用 Collections.synchronizedList ()(推荐用于一般并发场景)
Collections
是 Java 集合工具类,提供了static <T> List<T> synchronizedList(List<T> list)
方法,能将 “非线程安全的 List” 包装成 “线程安全的 List”。
它的实现原理是:创建一个SynchronizedList
内部类,对原 List 的所有方法进行 “同步包装”(用synchronized
代码块加锁,锁对象是mutex
)。
示例:
// 包装ArrayList为线程安全的List
List<String> safeList = Collections.synchronizedList(new ArrayList<>());// 多线程使用
public class SynchronizedListDemo {public static void main(String[] args) {List<String> safeList = Collections.synchronizedList(new ArrayList<>());// 线程1添加元素new Thread(() -> {for (int i = 0; i < 1000; i++) {safeList.add("元素" + i);}}).start();// 线程2遍历元素(注意:遍历仍需手动加锁,因为synchronizedList未同步迭代器)new Thread(() -> {synchronized (safeList) { // 手动加锁,避免ConcurrentModificationExceptionfor (String s : safeList) {System.out.println(s);}}}).start();}
}
关键注意点:Collections.synchronizedList()
返回的 List,其iterator()
方法返回的迭代器不是线程安全的,因此遍历(尤其是 foreach)时,需要手动用synchronized
加锁,否则仍可能抛出ConcurrentModificationException
。
优缺点:
- 优点:锁粒度比 Vector 小(
synchronized
代码块 vs 同步方法),效率更高,使用灵活(可包装任意 List 实现类); - 缺点:遍历需手动加锁,稍显繁琐,适合并发量不大的场景。
6.3 方案 3:使用 CopyOnWriteArrayList(推荐用于读多写少场景)
CopyOnWriteArrayList
是 JDK 1.5 后引入的线程安全 List,基于 “写时复制(Copy-On-Write)” 机制实现,核心原理是:
- 读操作:无需加锁,直接读取当前数组(因为数组是不可变的,每次写操作都会创建新数组);
- 写操作:先复制一份当前数组,在新数组上进行修改(add/remove),修改完成后,将底层数组引用指向新数组(用
volatile
修饰数组引用,保证线程可见性)。
示例:
// CopyOnWriteArrayList的add方法(核心源码)
public boolean add(E e) {final ReentrantLock lock = this.lock;lock.lock(); // 写操作加锁,避免多线程同时复制数组try {Object[] elements = getArray(); // 获取当前数组int len = elements.length;Object[] newElements = Arrays.copyOf(elements, len + 1); // 复制新数组newElements[len] = e; // 在新数组添加元素setArray(newElements); // 指向新数组return true;} finally {lock.unlock(); // 释放锁}
}// 多线程使用CopyOnWriteArrayList
public class CopyOnWriteArrayListDemo {public static void main(String[] args) {List<String> cowList = new CopyOnWriteArrayList<>();// 线程1添加元素(写操作)new Thread(() -> {for (int i = 0; i < 1000; i++) {cowList.add("元素" + i);}}).start();// 线程2遍历元素(读操作,无需加锁)new Thread(() -> {for (String s : cowList) {System.out.println(s);}}).start();}
}
优缺点:
- 优点:读操作无锁,效率极高,适合 “读多写少” 的场景(如系统配置、商品列表等);遍历无需加锁,不会抛出
ConcurrentModificationException
; - 缺点:写操作需要复制数组,消耗内存(如果数组较大,复制成本高);写操作加锁,并发写效率低;读操作可能读取到 “旧数据”(因为写操作是异步的,新数组未替换前,读的还是旧数组)。
3 种线程安全方案对比
方案 | 实现原理 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
Vector | 同步方法(synchronized) | 简单易用 | 锁粒度大,效率低 | 兼容旧代码,并发量极小场景 |
Collections.synchronizedList | 同步代码块(synchronized) | 效率高于 Vector,灵活 | 遍历需手动加锁 | 一般并发场景,读写均衡 |
CopyOnWriteArrayList | 写时复制(COW) | 读操作无锁,效率高 | 写操作耗内存,可能读旧数据 | 读多写少场景(如配置、列表展示) |
七、ArrayList 常见问题与避坑指南
即使掌握了 ArrayList 的基础用法,开发中仍可能因细节疏忽导致问题。下面梳理 6 个高频问题,帮你避坑。
7.1 问题 1:ArrayList 存储基本类型时的 “自动装箱 / 拆箱” 问题
ArrayList 只能存储引用类型,不能直接存储基本类型(如 int、long、boolean),如果尝试存储基本类型,Java 会自动进行 “装箱”(基本类型→包装类),取出时自动 “拆箱”(包装类→基本类型)。
但频繁装箱 / 拆箱会产生性能消耗,尤其是在大量数据操作时。
解决方案:
- 如果需要存储基本类型,优先使用
ArrayList
的 “基本类型专用版”—— 如IntArrayList
(来自 Eclipse Collections)、FastUtil
库的IntArrayList
等,这些类直接存储基本类型,避免装箱 / 拆箱; - 若必须用 JDK 自带类,尽量减少频繁的 add/get 操作,可批量处理。
示例:
// 错误:不能直接存储int(编译报错)
// List<int> list = new ArrayList<>();// 正确:存储Integer(自动装箱)
List<Integer> list = new ArrayList<>();
list.add(10); // 自动装箱:int 10 → Integer 10
int num = list.get(0); // 自动拆箱:Integer 10 → int 10
7.2 问题 2:ArrayList 的 toArray () 方法返回 Object [],无法直接强转
调用list.toArray()
时,返回的是Object[]
类型,不能直接强转为String[]
、Integer[]
等具体类型,否则会抛出ClassCastException
。
原因:数组的类型在创建时就已确定,Object[]
不能直接转为子类数组(如String[]
)。
解决方案:
- 使用
toArray(T[] a)
方法,传入一个指定类型的数组,返回该类型的数组。
示例:
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));// 错误:Object[]不能强转为String[]
// Object[] objArr = list.toArray();
// String[] strArr = (String[]) objArr; // 抛出ClassCastException// 正确:使用toArray(T[] a)
String[] strArr = list.toArray(new String[0]); // 传入空数组,返回String[]
System.out.println(Arrays.toString(strArr)); // 输出:[a, b, c]
注意:new String[0]
是 “占位符”,ArrayList 会根据集合大小创建一个新的String[]
数组;也可以传入new String[list.size()]
,避免创建额外的数组(性能略优)。
7.3 问题 3:迭代器遍历中修改集合结构,抛出 ConcurrentModificationException
如前文所述,迭代器(包括 foreach 底层的迭代器)遍历过程中,若用集合的add/remove
方法修改结构,会导致modCount != expectedModCount
,抛出ConcurrentModificationException
。
解决方案:
- 遍历过程中删除元素:用迭代器的
remove()
方法; - 遍历过程中添加元素:使用
ListIterator
(Iterator 的子类,支持添加元素)。
示例:用 ListIterator 在遍历中添加元素
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
// 获取ListIterator
ListIterator<String> listIterator = list.listIterator();while (listIterator.hasNext()) {String element = listIterator.next();if ("b".equals(element)) {// 用ListIterator的add()添加元素,不会抛出异常listIterator.add("d");}
}System.out.println(list); // 输出:[a, b, d, c]
7.4 问题 4:ArrayList 的 subList () 方法返回的子列表 “依赖原列表”
subList(int fromIndex, int toIndex)
方法返回原列表中[fromIndex, toIndex)
范围的子列表,但这个子列表不是独立的—— 它的底层仍然引用原列表的数组,修改子列表会影响原列表,反之亦然。
示例:
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
// 获取子列表:索引1到3(包含1,不包含3)→ [b, c]
List<String> subList = list.subList(1, 3);// 修改子列表:会影响原列表
subList.set(0, "x");
System.out.println(list); // 输出:[a, x, c, d]// 修改原列表:会影响子列表
list.add(2, "y");
System.out.println(subList); // 输出:[x, y, c](子列表的范围会跟着原列表变化)
注意事项:
- 子列表依赖原列表,原列表的结构(add/remove)修改后,子列表的所有操作(如 get、add)都会抛出
ConcurrentModificationException
; - 如果需要独立的子列表,建议将子列表复制到新的 ArrayList 中:
List<String> independentSubList = new ArrayList<>(subList);
。
7.5 问题 5:ArrayList 的 contains (null) 返回 true,但 indexOf (null) 可能返回 - 1?
不会!contains(null)
的底层是indexOf(null) >= 0
,如果集合中有 null 元素,indexOf(null)
会返回第一个 null 的索引,contains(null)
返回 true;如果没有 null 元素,indexOf(null)
返回 - 1,contains(null)
返回 false。
示例:
7.6 问题 6:ArrayList 序列化时,未使用 transient 修饰的元素会被序列化吗?
ArrayList 的elementData
用transient
修饰,这意味着默认序列化时,elementData
不会被序列化。但 ArrayList 重写了writeObject
和readObject
方法,手动序列化 “实际有值的元素”(即size
个元素),避免序列化空元素浪费空间。
示例:序列化与反序列化
public class ArrayListSerializationDemo {public static void main(String[] args) throws Exception {List<String> list = new ArrayList<>(100);list.add("a");list.add("b");// 序列化ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.txt"));oos.writeObject(list);oos.close();// 反序列化ObjectInputStream ois = new ObjectInputStream(new FileInputStream("list.txt"));List<String> deserializedList = (List<String>) ois.readObject();ois.close();System.out.println(deserializedList); // 输出:[a, b]System.out.println(getCapacity(deserializedList)); // 输出:2(反序列化后容量为size,而非100)}
}
可以看到,反序列化后的 ArrayList 容量为size
(2),而非原容量(100),说明序列化时只保存了实际元素,优化了空间。
八、总结:ArrayList 的核心要点与适用场景
通过本文的全面解析,我们可以将 ArrayList 的核心知识浓缩为以下几点:
1. 核心定位
- 底层是动态扩展的 Object 数组,支持随机访问(O (1) 查询),有序、可重复、非线程安全;
- 解决了普通数组 “容量固定” 的痛点,是 Java 中 “查询密集型场景” 的首选 List 实现。
2. 底层关键机制
- 扩容机制:默认初始容量 10(JDK 1.8 延迟初始化),扩容时默认扩大到 1.5 倍,不够则取最小需要容量;
- 缩容机制:无自动缩容,需手动调用
trimToSize()
; - 快速失败机制:通过
modCount
和expectedModCount
检测并发修改,避免数据不一致。
3. 线程安全解决方案
- 一般并发场景:用
Collections.synchronizedList()
; - 读多写少场景:用
CopyOnWriteArrayList
; - 旧代码兼容:用 Vector(不推荐)。
4. 适用场景与不适用场景
适用场景 | 不适用场景 |
---|---|
频繁查询(get)、尾部添加 / 删除(add/removeLast) | 频繁在中间插入 / 删除元素(O (n) 时间复杂度) |
单线程环境或可手动处理线程安全的场景 | 多线程并发写密集场景(如高并发订单创建) |
已知元素数量,可指定初始容量优化性能 | 需要 “线程安全且无读写延迟” 的场景 |
5. 开发避坑口诀
- 基本类型存包装,避免频繁装箱拆;
- 初始容量提前定,减少扩容耗性能;
- 迭代修改用迭代器,foreach 遍历不修改;
- 多线程下要注意,安全方案三选一;
- 子列表不独立,修改原表会出问题。
ArrayList 作为 Java 集合框架的 “基石”,看似简单,实则包含丰富的底层设计思想(如延迟初始化、写时复制、快速失败)。只有深入理解这些原理,才能在实际开发中灵活运用,写出高效、稳健的代码。希望本文能成为你掌握 ArrayList 的 “一站式指南”,助力你的 Java 学习与开发之路。