当前位置: 首页 > ds >正文

深入浅出 ArrayList:从基础用法到底层原理的全面解析(下)

五、ArrayList 底层核心原理 —— 扩容机制与性能优化

很多开发者使用 ArrayList 时,只知道 “它能自动扩容”,但不清楚扩容的具体逻辑 —— 比如 “什么时候扩容?”“扩容到多大?”“扩容过程消耗性能吗?”。理解这些底层原理,能帮助你写出更高效的代码。

5.1 扩容的触发条件

ArrayList 的扩容是在 “添加元素时” 触发的,核心入口是ensureCapacityInternal(int minCapacity)方法,minCapacity表示 “当前需要的最小容量”(即size + 1,因为要添加 1 个元素)。

扩容的核心逻辑可以概括为:

  1. 计算当前需要的最小容量minCapacity(添加 1 个元素就是size + 1,添加 n 个元素就是size + n);
  2. 比较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,此时elementDataDEFAULTCAPACITY_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. 新容量默认是旧容量的 1.5 倍(通过oldCapacity + (oldCapacity >> 1)计算,右移操作比乘法更高效);
  2. 如果 1.5 倍后的新容量仍小于 “最小需要容量”(比如一次性添加大量元素),则新容量直接等于 “最小需要容量”;
  3. 如果新容量超过MAX_ARRAY_SIZEInteger.MAX_VALUE - 8),则根据 “最小需要容量” 判断:如果minCapacity > MAX_ARRAY_SIZE,新容量取Integer.MAX_VALUE,否则取MAX_ARRAY_SIZE
  4. 通过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

解决方案

  1. 遍历过程中删除元素:用迭代器的remove()方法;
  2. 遍历过程中添加元素:使用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](子列表的范围会跟着原列表变化)

注意事项

  1. 子列表依赖原列表,原列表的结构(add/remove)修改后,子列表的所有操作(如 get、add)都会抛出ConcurrentModificationException
  2. 如果需要独立的子列表,建议将子列表复制到新的 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 的elementDatatransient修饰,这意味着默认序列化时,elementData不会被序列化。但 ArrayList 重写了writeObjectreadObject方法,手动序列化 “实际有值的元素”(即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()
  • 快速失败机制:通过modCountexpectedModCount检测并发修改,避免数据不一致。

3. 线程安全解决方案

  • 一般并发场景:用Collections.synchronizedList()
  • 读多写少场景:用CopyOnWriteArrayList
  • 旧代码兼容:用 Vector(不推荐)。

4. 适用场景与不适用场景

适用场景不适用场景
频繁查询(get)、尾部添加 / 删除(add/removeLast)频繁在中间插入 / 删除元素(O (n) 时间复杂度)
单线程环境或可手动处理线程安全的场景多线程并发写密集场景(如高并发订单创建)
已知元素数量,可指定初始容量优化性能需要 “线程安全且无读写延迟” 的场景

5. 开发避坑口诀

  • 基本类型存包装,避免频繁装箱拆;
  • 初始容量提前定,减少扩容耗性能;
  • 迭代修改用迭代器,foreach 遍历不修改;
  • 多线程下要注意,安全方案三选一;
  • 子列表不独立,修改原表会出问题。

ArrayList 作为 Java 集合框架的 “基石”,看似简单,实则包含丰富的底层设计思想(如延迟初始化、写时复制、快速失败)。只有深入理解这些原理,才能在实际开发中灵活运用,写出高效、稳健的代码。希望本文能成为你掌握 ArrayList 的 “一站式指南”,助力你的 Java 学习与开发之路。

http://www.xdnf.cn/news/18951.html

相关文章:

  • IDEA2022开启新版UI
  • 【嵌入式电机控制#进阶4】无感控制(二):观测器导论锁相环(全网最通俗易懂)
  • 【C++11】auto关键字:自动类型推导
  • MCP之weather server demo
  • 李沐-第十章-训练Seq2SeqAttentionDecoder报错
  • Leetcode top100之链表排序
  • 【ElasticSearch】json查询语法
  • 美团一面“保持好奇”
  • Spring Boot 项目打包成可执行程序
  • HTML应用指南:利用POST请求获取全国三星门店位置信息
  • Ubuntu安装及配置Git(Ubuntu install and config Git Tools)
  • Next.js 15.5.0:探索 Turbopack Beta、稳定的 Node.js 中间件和 TypeScript 的改进
  • 30.throw抛异常
  • 【图像算法 - 23】工业应用:基于深度学习YOLO12与OpenCV的仪器仪表智能识别系统
  • 【P2P】P2P主要技术及RELAY服务1:python实现
  • Kubernetes 构建高可用、高性能 Redis 集群
  • 线性回归入门:从原理到实战的完整指南
  • k8sday17安全机制
  • 真实应急响应案例记录
  • 一键终结Win更新烦恼!你从未见过如此强大的更新暂停工具!
  • PNP机器人介绍:全球知名具身智能/AI机器人实验室介绍之多伦多大学机器人研究所
  • PC端逆向会用到的常见伪指令
  • 解读 “货位清则标识明,标识明则管理成” 的实践价值
  • 灰狼算法+四模型对比!GWO-CNN-BiLSTM-Attention系列四模型多变量时序预测
  • EasyClick 生成唯一设备码
  • 【CV】图像基本操作——①图像的IO操作
  • XC95144XL-10TQG144I Xilinx XC9500XL 高性能 CPLD
  • 从0到1:用 Qwen3-Coder 和 高德MCP 助力数字文旅建造——国庆山西游
  • 我的小灶坑
  • Web程序设计