新闻详情

新闻详情

首页 / 资讯中心 / 详情

Java集合-02-ArrayList源码:扩容、System.arraycopy 与 fail-fast

发布时间:2026/9/30 7:23:13来源:尧图网络
Java集合-02-ArrayList源码:扩容、System.arraycopy 与 fail-fast
1. 结论先行ArrayList 本质是动态数组ArrayList 是 Java 集合框架中最常用的 List 实现之一其底层本质是一个可动态扩容的对象数组。它之所以查询快、增删慢根源就在于这个数组结构数组支持按下标 O(1) 随机访问但中间插入和删除需要整体挪动元素。一句话总结ArrayList 数组 扩容机制 迭代器保护机制。理解这三个部分就理解了 ArrayList 的核心。本文从源码角度拆解 ArrayList 的扩容、System.arraycopy 挪位和 fail-fast 机制并配图说明帮助你把源码读透。2. 核心字段先看 ArrayList 的几个关键字段它们是理解后续所有逻辑的基础。// 默认初始容量 private static final int DEFAULT_CAPACITY 10; // 空数组无参构造时使用 private static final Object[] EMPTY_ELEMENTDATA {}; // 默认容量空数组懒加载时使用 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA {}; // 真正存储元素的数组 transient Object[] elementData; // 元素个数 private int size; // 结构性修改次数fail-fast 核心 protected transient int modCount 0;这里有两个容易混淆的空数组EMPTY_ELEMENTDATA用于指定容量为 0 的构造DEFAULTCAPACITY_EMPTY_ELEMENTDATA用于无参构造。两者的区别在于无参构造的数组在第一次 add 时会扩容到默认容量 10而指定容量 0 的数组则按 0 容量起步。3. 构造方法ArrayList 提供了三个构造方法分别对应不同的初始化场景。3.1 无参构造public ArrayList() { this.elementData DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }无参构造只是把 elementData 指向一个空数组并没有真正分配 10 个容量的空间。这就是懒加载容量 10 的数组在第一次 add 时才真正创建。3.2 指定容量构造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); } }指定容量大于 0 时直接创建对应大小的数组等于 0 时使用空数组小于 0 时抛出异常。3.3 传入集合构造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 { elementData EMPTY_ELEMENTDATA; } }传入集合时直接把集合元素拷贝到新数组。如果传入的本身就是 ArrayList则直接复用其内部数组不复制元素否则通过 Arrays.copyOf 复制。4. 添加元素添加元素是 ArrayList 最核心的操作之一分为尾部追加和指定位置插入两种。4.1 add(E)尾部追加public boolean add(E e) { ensureCapacityInternal(size 1); // 确保容量足够 elementData[size] e; // 赋值并 size return true; }尾部追加的逻辑很简单先确保容量够用然后在下标 size 处赋值最后 size 自增。整个过程是 O(1) 摊还复杂度。4.2 add(int, E)指定位置插入public void add(int index, E element) { rangeCheckForAdd(index); // 越界检查 ensureCapacityInternal(size 1); // 关键把 index 及之后的元素整体后移一位 System.arraycopy(elementData, index, elementData, index 1, size - index); elementData[index] element; size; }指定位置插入需要先把 index 之后的元素整体后移一位再在 index 处赋值。这个挪位操作是 O(n) 的正是 ArrayList 中间插入慢的根本原因。下面用图说明 System.arraycopy 的挪位过程flowchart LR A[原数组: [A, B, C, D, E]] -- 在 index2 插入 X -- B[System.arraycopy 把 C,D,E 后移] B -- C[后移结果: [A, B, C, C, D, E]] C -- 在 index2 赋值 X -- D[最终: [A, B, X, C, D, E]]5. 扩容机制扩容是 ArrayList 最值得深入的部分也是面试高频考点。5.1 懒加载第一次 add 才扩到 10无参构造创建的 ArrayList 初始指向空数组第一次 add 时才真正分配容量 10 的数组。这就是懒加载也是面试中容易踩坑的点。private void ensureCapacityInternal(int minCapacity) { if (elementData DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { modCount; // 结构性修改计数 if (minCapacity - elementData.length 0) { grow(minCapacity); } }当 elementData 还是默认空数组时minCapacity 会被提升到 DEFAULT_CAPACITY10从而在第一次 add 时扩容到 10。5.2 grow()1.5 倍扩容private void grow(int minCapacity) { int oldCapacity elementData.length; // 新容量 旧容量 旧容量右移一位 旧容量的 1.5 倍 int newCapacity oldCapacity (oldCapacity 1); if (newCapacity - minCapacity 0) { newCapacity minCapacity; } if (newCapacity - MAX_ARRAY_SIZE 0) { newCapacity hugeCapacity(minCapacity); } // 拷贝到新数组 elementData Arrays.copyOf(elementData, newCapacity); }扩容的核心公式是newCapacity oldCapacity (oldCapacity 1)即每次扩容为原来的 1.5 倍。例如 10 扩容到 1515 扩容到 2222 扩容到 33。下面用图说明 1.5 倍扩容过程flowchart LR A[容量 10 已用 10] -- add 第 11 个元素 -- B[grow() 计算 newCapacity 10 5 15] B -- Arrays.copyOf 拷贝 -- C[新数组容量 15 旧元素全部搬入] C -- 继续 add -- D[容量 15 用满后 再扩到 22]5.3 MAX_ARRAY_SIZE 与 OutOfMemoryErrorprivate static final int MAX_ARRAY_SIZE Integer.MAX_VALUE - 8; private static int hugeCapacity(int minCapacity) { if (minCapacity 0) { throw new OutOfMemoryError(); // 溢出 } return (minCapacity MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }当扩容后的容量超过 MAX_ARRAY_SIZEInteger.MAX_VALUE - 8时会尝试使用更大的容量如果 minCapacity 已经溢出为负数则抛出 OutOfMemoryError。MAX_ARRAY_SIZE 预留 8 个位置是为了容纳对象头等 JVM 开销。6. 删除元素删除元素同样涉及数组挪位是 O(n) 操作。6.1 remove(int)按下标删除public E remove(int index) { rangeCheck(index); modCount; E oldValue elementData(index); int numMoved size - index - 1; if (numMoved 0) { // 把 index 之后的元素整体前移一位 System.arraycopy(elementData, index 1, elementData, index, numMoved); } elementData[--size] null; // 置空帮助 GC return oldValue; }按下标删除时把 index 之后的元素整体前移一位然后把最后一个位置置空并 size 减一。置空操作是为了让 GC 可以回收不再引用的对象。6.2 remove(Object)遍历查找后删除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; }按对象删除时先遍历数组找到目标元素再调用 fastRemove 删除。这里用 equals 比较所以自定义对象需要正确重写 equals 方法。6.3 fastRemove跳过越界检查的快速删除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; }fastRemove 与 remove(int) 逻辑相同只是跳过了越界检查因为调用方已经确认 index 合法。7. 查询与修改查询和修改是 ArrayList 的优势所在因为数组支持按下标随机访问。public E get(int index) { rangeCheck(index); return elementData(index); // 直接按下标取 } public E set(int index, E element) { rangeCheck(index); E oldValue elementData(index); elementData[index] element; return oldValue; }get 和 set 都是直接通过下标访问数组元素时间复杂度为 O(1)。这正是 ArrayList 查询快的原因不需要像链表那样从头遍历。8. 迭代器与 fail-fast迭代器是 ArrayList 中另一个高频考点尤其是 fail-fast 机制。8.1 Itr 的核心字段private class Itr implements IteratorE { int cursor; // 下一个要返回的元素下标 int lastRet -1; // 上一次返回的元素下标-1 表示没有 int expectedModCount modCount; // 期望的结构修改次数 }Itr 维护三个关键字段cursor 记录下一个元素位置lastRet 记录上一次返回位置expectedModCount 记录创建迭代器时的 modCount。8.2 checkForComodificationfail-fast 核心final void checkForComodification() { if (modCount ! expectedModCount) { throw new ConcurrentModificationException(); } }每次调用 next() 或 remove() 时都会检查 modCount 是否等于 expectedModCount。如果期间发生了结构性修改如 add、remove、clearmodCount 会变化从而抛出 ConcurrentModificationException。下面用流程图说明 fail-fast 机制flowchart TD A[创建迭代器 expectedModCount modCount] -- B[调用 next()] B -- C{modCount expectedModCount?} C -- 是 -- D[正常返回元素] C -- 否 -- E[抛出 ConcurrentModificationException] D -- B8.3 为什么 for-each 中删除会抛异常for-each 底层就是使用迭代器遍历。如果在遍历过程中调用 list.remove()会修改 modCount导致迭代器检测到 modCount 与 expectedModCount 不一致从而抛出 ConcurrentModificationException。// 这段代码会抛 ConcurrentModificationException for (String s : list) { if (s.equals(b)) { list.remove(s); // 直接调用 list.removemodCount 变化 } }8.4 正确删除方式正确的删除方式有两种使用 Iterator.remove() 或使用 removeIf。// 方式一Iterator.remove() IteratorString it list.iterator(); while (it.hasNext()) { String s it.next(); if (s.equals(b)) { it.remove(); // 会同步更新 expectedModCount } } // 方式二removeIfJDK 8 list.removeIf(s - s.equals(b));Iterator.remove() 之所以安全是因为它在删除后会同步更新 expectedModCount保持与 modCount 一致。removeIf 内部也做了同样的处理。9. subList 视图坑subList 返回的是原 List 的视图而不是独立副本这是一个容易踩坑的地方。ListString sub list.subList(0, 3); // 对 sub 的任何结构性修改都会反映到原 list 上 sub.add(x); // 原 list 也会多一个元素更危险的是如果 subList 创建后原 list 发生了结构性修改再操作 subList 会抛出 ConcurrentModificationException。因为 subList 内部也维护了 expectedModCount。10. 复杂度总结与使用场景下表总结了 ArrayList 各操作的时间复杂度操作时间复杂度说明get(index)O(1)数组按下标随机访问set(index, e)O(1)数组按下标赋值add(e) 尾部追加O(1) 摊还扩容时 O(n)但均摊 O(1)add(index, e)O(n)需要挪动元素remove(index)O(n)需要挪动元素remove(Object)O(n)先遍历查找再挪位contains(Object)O(n)线性遍历适用场景频繁按下标查询、尾部增删、元素数量可预估的场景。不适用场景频繁在中间插入/删除、需要频繁按值查找的场景此时应考虑 LinkedList 或 HashMap。11. 面试题速答最后整理几个高频面试题帮助快速复习。Q1默认容量是多少什么时候初始化默认容量是 10但无参构造并不会立即创建容
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

ARP欺骗实验完整实操:从环境搭建到中间人攻击验证 2026/9/30 10:11:47

ARP欺骗实验完整实操:从环境搭建到中间人攻击验证

简介:西南科技大学网络攻防与对抗课程实验三的ARP欺骗实验报告,面向需要完成验证型实验或学习网络攻防基础的高校学生。报告基于Cain与Winpcap工具,完整还原了双虚拟机攻防环境下的ARP欺骗流程,从地址解析协议原理、攻击者与被攻击…

阅读更多 →
软考系统分析师真题回忆版的命题逻辑与能力训练方法 2026/9/30 10:11:47

软考系统分析师真题回忆版的命题逻辑与能力训练方法

简介:本资源为2023年软考高级系统分析师考试真题回忆版解析资料,面向备考该职称考试的IT从业者、系统架构师及高校相关专业考生,聚焦综合知识、案例分析与论文写作三大模块的高频考点与解题逻辑。资料以1份15KB的Word文档(.docx&a…

阅读更多 →
平台化构建智能体:低代码开发落地企业AI应用的实战路径 2026/9/30 10:11:41

平台化构建智能体:低代码开发落地企业AI应用的实战路径

过去这一年,我明显感觉到一个趋势:圈子里讨论的焦点,已经从"大模型能干什么"彻底转向了"智能体怎么落地"。但真正跑通业务、能被一线同事日常使用的智能体,十有八九不是纯代码一行行敲出来的,而是…

阅读更多 →
ISO 8601时间格式的深层契约与分布式系统时区避坑指南 2026/9/30 10:11:41

ISO 8601时间格式的深层契约与分布式系统时区避坑指南

1. 这个看似“标准”的时间格式,其实藏着最常被忽略的系统性陷阱你有没有在日志里看到过这样的时间戳:2024-03-15T14:27:38.12308:00?或者在API响应体中反复撞见2023-12-01T09:05:44.999Z?很多人第一反应是:“哦&#…

阅读更多 →
Model-Optimizer实战指南:大模型推理端到端加速方法论 2026/9/30 10:11:40

Model-Optimizer实战指南:大模型推理端到端加速方法论

1. 项目概述:Model-Optimizer 不是工具名,而是一类工程实践的统称 “Model-Optimizer”这个名称乍看像某个开源项目或商业软件,但实际在NVIDIA生态和大模型推理部署一线,它根本不是一款可下载安装的独立产品——而是工程师在真实生…

阅读更多 →
机器视觉驱动的消防炮自动闭环控制系统 2026/9/30 10:11:40

机器视觉驱动的消防炮自动闭环控制系统

简介:本资源是一份面向消防自动化系统研发人员、智能装备控制工程师及高校机电/自动化专业师生的技术方案文档,聚焦解决传统消防炮在火源定位与射流落点校正中精确性不足、环境适应性差等核心痛点。文档提出一种融合双目视觉定位与图像特征反馈的混合闭环…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉