ArrayList 底层原理

ArrayList 底层原理

目标:面试里把 ArrayList 讲到"源码级",包含:数据结构、扩容机制、add/remove/get/set、fail-fast、迭代器、subList 坑、和 LinkedList/Vector/CopyOnWriteArrayList 对比,以及高频追问话术。


1. ArrayList 是什么?一句话定义

  • 基于动态数组(Object[])实现的可变长线性表
  • 优点:随机访问快(O(1)),遍历快,内存连续、CPU cache 友好。
  • 缺点:中间插入/删除慢(O(n)),扩容有成本,线程不安全。

2. 核心字段(源码必会)

2.1 关键字段

  • transient Object[] elementData;:底层数组(真正存放元素)
  • private int size;:当前元素个数(不是数组长度)
  • protected transient int modCount;(继承自 AbstractList):结构性修改次数,用于 fail-fast

2.2 两个"空数组"常量(JDK8 常见)

  • EMPTY_ELEMENTDATA:默认构造时用(容量 0)
  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA:区分"默认构造但还没真正分配容量"的状态

面试点:默认 new ArrayList<>() 并不会立刻创建长度为 10 的数组,而是第一次 add 时才创建默认容量。


3. 构造函数行为(很容易被问)

3.1 new ArrayList()

  • elementData 先指向 DEFAULTCAPACITY_EMPTY_ELEMENTDATA(空数组)
  • 第一次 add 时,分配默认容量 10

3.2 new ArrayList(int initialCapacity)

  • initialCapacity > 0:直接分配对应长度数组
  • initialCapacity == 0:使用 EMPTY_ELEMENTDATA
  • < 0:抛 IllegalArgumentException

3.3 new ArrayList(Collection<? extends E> c)

  • 直接 c.toArray() 拷贝
  • 注意:toArray() 返回 Object[],如果不是 Object[] 会再 Arrays.copyOf 一次

4. add 流程(主干一定会)

4.1 add(E e)

核心步骤:

  1. ensureCapacityInternal(size + 1):确保能放下新元素
  2. elementData[size++] = e:尾插
  3. modCount++(结构性修改)

时间复杂度:均摊 O(1)(amortized O(1)),扩容那次是 O(n)。

4.2 add(int index, E element)

核心步骤:

  1. 校验 index
  2. 扩容检查
  3. System.arraycopy(...) 把 index 之后整体右移一位
  4. 放入新元素,size++modCount++

时间复杂度:O(n)(移动元素)。


5. 扩容机制(面试高频,必须说出 1.5 倍)

5.1 扩容触发

minCapacity > elementData.length 时扩容。

5.2 新容量计算(JDK8)

  • newCapacity = oldCapacity + (oldCapacity >> 1)
  • 等价于 1.5 倍扩容

并且:

  • newCapacity < minCapacity:直接用 minCapacity
  • 若超过 MAX_ARRAY_SIZE(接近 Integer.MAX_VALUE):走 hugeCapacity 处理,避免溢出

5.3 拷贝成本

扩容本质是:新建更大数组 + Arrays.copyOf(底层 System.arraycopy)把旧数组拷贝过去

  • 扩容那一次是 O(n)
  • 但总体均摊下来,尾插仍然是均摊 O(1)

5.4 面试加分点:如何减少扩容?

  • 你知道大概容量:用 new ArrayList<>(capacity)ensureCapacity(capacity)
  • 场景:批量导入、分页聚合、预期固定条数的缓存等。

6. get / set / remove / clear(常问复杂度)

6.1 get(int index)

  • 直接 elementData[index]
  • 时间复杂度:O(1)

6.2 set(int index, E element)

  • 覆盖旧值,返回旧值
  • 时间复杂度:O(1)

6.3 remove(int index)

步骤:

  1. 取旧值
  2. System.arraycopy 左移覆盖(index 之后整体左移一位)
  3. elementData[--size] = null(帮助 GC)
  4. modCount++

时间复杂度:O(n)

6.4 remove(Object o)

  • 找到第一次匹配的元素(equals)后,调用 fastRemove(index) 做左移
  • 最坏:O(n)

6.5 clear()

  • [0..size) 全部置 nullsize=0
  • 时间复杂度:O(n)(要清引用)

7. fail-fast:为什么 foreach 删除会报 ConcurrentModificationException?(必考)

7.1 机制

  • 迭代器创建时捕获 expectedModCount = modCount
  • 每次 next()/remove() 都会检查 modCount == expectedModCount
  • 如果你在迭代过程中,使用 list 的 add/remove 直接改结构,modCount 变了,迭代器就炸:ConcurrentModificationException

7.2 正确删除方式

  • 用迭代器自己的 Iterator.remove()(会同步更新 expectedModCount)
  • 或者用 removeIf(...)
  • 或者倒序 for(按 index 删除)

面试一句话:fail-fast 是"尽快失败",不是并发安全保证。并发下它可能不抛,也可能抛。


8. subList 的坑(高级开发很爱问,踩过才懂)

8.1 subList 不是拷贝,是视图

subList(from, to) 返回的是 SubList 视图,底层仍引用原 list 的数组。

8.2 常见坑

  1. 结构性修改冲突:对原 list 做 add/remove,subList 再操作会触发 CME(因为 modCount 不一致)
  2. subList 转 ArrayList:
    • new ArrayList<>(list.subList(...)) 才是拷贝(安全)

记法:subList = view,不是副本。


9. 线程安全相关:ArrayList 为啥不安全?怎么解决?

9.1 典型并发问题

两个线程同时 add

  • 都读到相同的 size
  • 写入同一个 index
  • size++ 竞争导致丢数据或覆盖
  • 扩容时更容易出问题

9.2 解决方案(按场景)

  • 读多写少:CopyOnWriteArrayList
  • 简单互斥:Collections.synchronizedList(new ArrayList<>())
  • 更通用:外部加锁(ReentrantLock)或用线程安全容器/队列

10. ArrayList vs LinkedList vs Vector vs CopyOnWriteArrayList(面试必比)

10.1 ArrayList vs LinkedList

  • 随机访问:ArrayList O(1);LinkedList O(n)
  • 中间插入删除:理论 LinkedList O(1)(已定位节点),但定位是 O(n),且对象分散、cache 不友好
  • 遍历:ArrayList 通常更快(内存连续)

面试真相:大多数业务场景,ArrayList 更快,LinkedList 很少是最优。

10.2 ArrayList vs Vector

  • Vector 方法级 synchronized:线程安全但性能差
  • Vector 扩容策略:很多实现是 2 倍(也可通过构造参数指定增量)
  • 现代基本不用 Vector

10.3 ArrayList vs CopyOnWriteArrayList

  • 写:COW 每次写都会复制整个数组(O(n)),非常贵
  • 读:无锁读取,迭代器是快照(不会 CME)
  • 适合:读多写少、元素量不大且写不频繁(如配置、监听器列表)

11. 高频追问(直接背答案)

Q1:ArrayList 默认容量是多少?什么时候分配?

  • 默认构造时容量是 0(空数组)
  • 第一次 add 时分配默认容量 10(JDK8 常见)

Q2:扩容为什么是 1.5 倍?

  • 比 2 倍更省内存;比固定增量更少扩容次数
  • 1.5 倍是时间与空间的折中

Q3:为什么 remove 要把最后一个置 null?

  • 解除引用,帮助 GC 回收对象,避免内存泄漏

Q4:foreach 删除为什么报 CME?

  • fail-fast:迭代器的 expectedModCount 和 modCount 不一致
  • 正确方式用 iterator.remove / removeIf / 倒序删除

Q5:subList 有啥坑?

  • subList 是视图,和原 list 共享数组
  • 原 list 结构性修改会导致 subList 操作 CME
  • 要独立列表:new ArrayList<>(subList)

12. 一段"面试口播模板"(建议你直接练)

ArrayList 底层是 Object[] 动态数组,size 表示元素个数。add 尾插先 ensureCapacity,容量不够就按 1.5 倍扩容并拷贝数组,所以尾插是均摊 O(1),但扩容那次是 O(n)。随机访问 get/set 是 O(1),中间插入删除需要 arraycopy 移动元素是 O(n)。它不是线程安全的;遍历时如果结构性修改会因为 modCount/expectedModCount 触发 fail-fast 的 ConcurrentModificationException。subList 返回的是视图不是拷贝,和原 list 共享数组,原 list 结构性修改会影响 subList。


13. 面试加分:你真的会用的建议

  • 预估容量就提前设置,能少很多扩容拷贝成本(尤其在大列表聚合、批量导入)
  • 删除大量元素:优先 removeIf 或先过滤后新建列表(避免 n 次 arraycopy)
  • 并发读多写少:COW;并发写多:别硬上 list,考虑队列、分片、锁策略或其他数据结构

14. 源码关键方法(面试前扫一遍就稳)

  • add(E e)add(int,E)
  • ensureCapacityInternalgrow
  • remove(int)fastRemove
  • iterator()Itr.checkForComodification
  • subList(SubList 的实现)

相关推荐
Fleshy数模3 分钟前
从数据获取到突破限制:Python爬虫进阶实战全攻略
java·开发语言
像少年啦飞驰点、13 分钟前
零基础入门 Spring Boot:从“Hello World”到可上线的 Web 应用全闭环指南
java·spring boot·web开发·编程入门·后端开发
苍煜16 分钟前
万字详解Maven打包策略:从基础插件到多模块实战
java·maven
有来技术20 分钟前
Spring Boot 4 + Vue3 企业级多租户 SaaS:从共享 Schema 架构到商业化套餐设计
java·vue.js·spring boot·后端
东东51641 分钟前
xxx医患档案管理系统
java·spring boot·vue·毕业设计·智慧城市
一个响当当的名号1 小时前
lectrue9 索引并发控制
java·开发语言·数据库
进阶小白猿2 小时前
Java技术八股学习Day30
java·开发语言·学习
hhy_smile2 小时前
Class in Python
java·前端·python
qq_12498707533 小时前
基于Srpingboot心晴疗愈社平台的设计与实现(源码+论文+部署+安装)
java·数据库·spring boot·spring·microsoft·毕业设计·计算机毕业设计
大爱编程♡3 小时前
SpringBoot统一功能处理
java·spring boot·后端