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但无参构造并不会立即创建容

相关新闻

友为合同管理系统 | 模板与条款标准化——上线前最重要的一步

友为合同管理系统 | 模板与条款标准化——上线前最重要的一步

01 引言:模板乱了,系统就白建了核心观点:合同模板和条款的标准化,是合同管理系统能否真正发挥价值的基础。现实情况却往往令人担忧。企业真正有多少模板、散落在哪些位置、哪些存在版本冲突,法务部门往往并不清楚。大量…

2026/9/30 7:23:18 阅读更多 →
流水灯控制实验

流水灯控制实验

实验1 流水灯控制实验 一、实验目的 掌握STM32 GPIO寄存器底层原理,理解GPIO端口寄存器地址、配置参数。掌握寄存器方式编写GPIO输出驱动,实现LED流水灯。学会STM32标准外设库工程建立、库文件添加方法,标准库方式驱动LED。掌握HAL库外部中断…

2026/9/30 7:23:18 阅读更多 →
《Linux 网络编程》深入理解 IO 多路复用:select 函数详解与 Echo 服务实战

《Linux 网络编程》深入理解 IO 多路复用:select 函数详解与 Echo 服务实战

🔥小叶-duck:个人主页 ❄️个人专栏:《Data-Structure-Learning》《C入门到进阶&自我学习过程记录》 《Linux系统从入门到实践》《Linux网络从入门到实践》 《Qt 方寸极境》 《MySQL》 ✨未择之路,不须回头 已择之路&#xf…

2026/9/30 7:22:17 阅读更多 →

最新新闻

Linux软硬链接本质:inode与路径的底层原理

Linux软硬链接本质:inode与路径的底层原理

1. 为什么软硬链接不是“复制”,而是“指针”——从文件系统底层讲清楚你有没有试过用ln命令创建一个链接,结果发现删掉源文件后,软链接打不开、硬链接还能访问?或者反过来,改了软链接指向的文件,硬链接却毫…

2026/9/30 7:56:32 阅读更多 →
Linux软链接与硬链接的本质区别及实战应用

Linux软链接与硬链接的本质区别及实战应用

1. 为什么软链接和硬链接不是“差不多就行”的替代品?在Linux系统里,软链接(symbolic link)和硬链接(hard link)常被新手统称为“快捷方式”,但这种类比会埋下严重隐患。我刚入行时就吃过亏&…

2026/9/30 7:56:32 阅读更多 →
SAP S/4HANA部署方式选择实战决策指南

SAP S/4HANA部署方式选择实战决策指南

简介:本资源是一份面向SAP系统实施顾问、云架构师及企业数字化转型从业者的S/4HANA云部署方式深度解析文档,聚焦SAP官方当前主流的四种部署模型及其业务定位差异,有效解决企业在上云路径选择中的决策困惑。文档以清晰对比方式展开&#xff1a…

2026/9/30 7:56:32 阅读更多 →
uni-app 登录页 UI 精修:从背景层到页面栈的细节实战

uni-app 登录页 UI 精修:从背景层到页面栈的细节实战

uni-app 里给微信小程序做登录页面,第一版基本都停留在“能用”的阶段:一个 logo、两个输入框、一个按钮,收工。等产品拿着竞品截图走过来,说一句“这个登录页面 UI 好看,你照着改一下”,很多人才发现自己在…

2026/9/30 7:56:32 阅读更多 →
uni-app微信小程序登录页:Vue3纯CSS高转化UI实战

uni-app微信小程序登录页:Vue3纯CSS高转化UI实战

做小程序登录页这件事,我前后推倒重来过至少七个版本。第一版是照着教程堆出来的深色背景配白色输入框,自认为挺"高级",结果上线一周后后台数据显示登录页跳出率接近四成;第二版换了配色,数据没动&#xff1…

2026/9/30 7:56:32 阅读更多 →
拆开这个环:优化 RAG 通道,解开 SEO 与 GEO 的死循环

拆开这个环:优化 RAG 通道,解开 SEO 与 GEO 的死循环

摘要:GEO 没有标准,它只是 RAG 的引用打分回路。这个回路正在反向磨平人类内容:RLHF 把 AI 磨成平均值,GEO 再让人类照着这个平均值生长,两个平均化首尾相接,咬成死循环。解法不是拒绝 AI,也不是…

2026/9/30 7:55:32 阅读更多 →

日新闻

Base64 图片头部特征识别:从文件头到格式判断的完整指南

Base64 图片头部特征识别:从文件头到格式判断的完整指南

1. 项目概述:为什么说看懂 base64 图片头部是基本功这几年跟 base64 打交道的机会越来越多,后端接口返回图片、前端渲染验证码、小程序里存小图、还有一些老系统导出报表,动不动就给你一段长到怀疑人生的 base64 字符串。很多人拿到字符串就直…

2026/9/30 0:00:35 阅读更多 →
Java公交站牌广告管理系统:JSP+Servlet+MySQL实战落地指南

Java公交站牌广告管理系统:JSP+Servlet+MySQL实战落地指南

简介:本资源是一份面向Java初学者与课程设计学生的公交站牌广告灯箱管理系统毕业设计文档,聚焦城市公共广告资源信息化管理痛点,提供从需求分析到技术实现的完整方案。文档采用标准学术论文结构,含摘要、英文摘要、目录及五章正文…

2026/9/30 0:00:35 阅读更多 →
用 Redis Lua 构建大模型 API 多租户原子配额治理体系

用 Redis Lua 构建大模型 API 多租户原子配额治理体系

我去年年底接了一个内部 AI 平台的治理需求,背景很直接:公司把 DeepSeek、MiniMax 这类大模型 API 统一封装成内部网关,开放给几个业务团队用。结果第一个月账单出来,额度直接超了 4 倍。仔细查日志,发现原因并不复杂—…

2026/9/30 0:00:35 阅读更多 →

周新闻

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/29 8:16:59 阅读更多 →
SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/9/29 16:41:41 阅读更多 →
FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏 【免费下载链接】FireRed-OpenStoryline FireRed-OpenStoryline is an AI video editing agent that transforms manual editing into intention-driven directing through natural language …

2026/9/29 8:24:48 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/29 19:29:29 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/29 5:58:00 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/29 3:55:56 阅读更多 →