首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >面试基础整理之 ArrayList

面试基础整理之 ArrayList

作者头像
伯灵
发布2026-01-21 10:00:54
发布2026-01-21 10:00:54
1080
举报

一、先把“底层地图”记住(必须能画在白板上)

  1. 本质:动态数组(可变长度的数组),内部用 Object[] elementData 存储。
  2. 初始容量:默认构造器不立刻分配数组(直到首次 add);new ArrayList<>(initialCapacity) 会分配。JDK 常见实现默认初始容量为 10(在某些场景延迟分配为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA)。
  3. size 与 capacitysize 是实际元素数,capacity 是数组长度;size <= capacity
  4. 扩容触发:当 size + 1 > elementData.length 时触发扩容。
  5. 扩容策略:新容量 newCapacity = oldCapacity + (oldCapacity >> 1)(即约 1.5 倍)。
  6. 线程:非线程安全;并发修改可能导致数据错乱或抛异常。
  7. Iterator 的 fail-fast:通过 modCount 记录结构性修改,Iterator 在 next() 时检查 expectedModCountmodCount 是否一致,不一致则抛 ConcurrentModificationException

二、关键方法与伪源码(只看这几段就够问答)

注:下面是简化伪码,掌握核心逻辑与关键行含义即可在面试解释。

1) add(E e)
代码语言:javascript
复制
public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    modCount++;
    return true;
}

ensureCapacityInternal 会判断是否需要扩容,若需要则调用 grow()

2) grow()
代码语言:javascript
复制
private void grow(int minCapacity) {
    int oldCap = elementData.length;
    int newCap = oldCap + (oldCap >> 1); // 1.5x
    if (newCap < minCapacity) newCap = minCapacity;
    elementData = Arrays.copyOf(elementData, newCap);
}

关键点:使用 Arrays.copyOf 复制旧数组到新数组,时间 O(n)。

3) remove(int index)
代码语言:javascript
复制
public E remove(int index) {
    rangeCheck(index);
    E oldValue = (E) elementData[index];
    int numMoved = size - index - 1;
    if (numMoved > 0) 
        System.arraycopy(elementData, index + 1, elementData, index, numMoved);
    elementData[--size] = null; // help GC
    modCount++;
    return oldValue;
}

说明:删除中间元素会把后面元素整体左移,时间 O(n)。

4) Iterator (fail-fast 检查)
代码语言:javascript
复制
public E next() {
    if (modCount != expectedModCount) throw new ConcurrentModificationException();
    return elementData[cursor++];
}

说明:expectedModCount 在 iterator 创建时等于 modCount,若结构变化(add/remove)且不是通过 iterator 自身的 remove,则会检测到并抛异常。


三、面试高频问答与标准答案(可直接背)

下面把常见问题做成一句话 + 展开说明的模板,面试直接背就行。

Q1:ArrayList 底层是什么?优缺点?

一句话: 底层是动态数组,优点是随机访问快(O(1)),缺点是中间插入/删除代价高(O(n)),且扩容和复制耗时。 展开(可讲 2 行): 随机访问靠索引直接 elementData[i],时间 O(1)。插入/删除需移动数组元素(System.arraycopy),最差 O(n)。扩容时会分配更大的数组并复制元素,耗时且可能导致短期内内存占用增大。

Q2:扩容机制(什么时候扩容、扩多大、为什么 1.5 倍)?

一句话: 扩容在 add 导致 size > capacity 时触发,常用策略是 new = old + old>>1(1.5 倍)。 展开: 1.5 倍是在时间与空间之间折中:比 2 倍少内存浪费,但比小增量减少扩容次数,降低复制频率。

Q3:为什么不是线程安全?怎么保证线程安全?

一句话: 多线程写会并发修改 elementDatasize 导致数据竞态与不一致,ArrayList 没有锁。 展开: 若多线程并发 add/resize,会出现覆盖、数据丢失或索引越界。解决方法:Collections.synchronizedList(new ArrayList<>())(外部加锁)或使用 CopyOnWriteArrayList(写时复制,适合读多写少场景)。

Q4:fail-fast 是如何实现的?有什么局限?

一句话: 通过 modCount 和 iterator 的 expectedModCount 对比,检测到并发的结构性修改则抛 ConcurrentModificationException展开: fail-fast 只是“侦测”并发修改并快速失败,不保证绝对一致性(不是并发安全策略),只能作为调试/保护机制。

Q5:增删操作的时间复杂度?
  • add(e)(末尾):摊销 O(1)
  • add(index, e) / remove(index):O(n)(需要移动元素)
  • get(index):O(1)
  • contains(obj):O(n)

四、白板演示要点(面试展示技巧)

当面试官让你“讲讲 ArrayList”或“画个结构图”:

  1. 画一个数组框,标注 elementData,画出 size=4, capacity=8 的样子。
  2. 画箭头示范 add(如果 capacity 满了 -> 调用 grow -> 新数组 copy)。
  3. 演示 remove(1):元素 2、3、4 左移并 size--,并说明 elementData[size]=null 用于 GC。
  4. 说明 Iterator 如何通过 modCount 检测并发修改(画出 modCount 框和 expectedModCount)。 (这样图文并茂,面试官印象分高)

五、真题/练习题(练习后对答案)

做题可以巩固记忆。下面 6 道题,面试常见。答案写在题后。

练习 1(短答)

ArrayList 和 LinkedList 的 3 个主要区别? 答案要点:内存结构(数组 vs 链表)、随机访问(O(1) vs O(n))、插入删除(中间插入删除 ArrayList O(n),LinkedList O(1) 若有节点引用)、空间开销、缓存局部性(数组好)。

练习 2(代码推演)

给定 ArrayList<Integer> a = new ArrayList<>(2); a.add(1); a.add(2); a.add(3); 解释发生了什么。 答案要点:初始 capacity=2,第三次 add 时触发扩容,newCapacity ≈ 3(2 + 1 = 3)或按 1.5 规则变为 3;数组复制然后插入 3;size = 3。

练习 3(复杂度)

为什么 addAll(Collection c) 有时不是 O(m)?(m = c.size()) 答案要点:若需要扩容,可能多次触发数组复制;但实现通常会一次性 ensureCapacity(size + c.size()),能把复制控制为一次,平均 O(m)。

练习 4(并发)

在并发环境中,为什么遍历 ArrayList 时即使没抛 CME 也可能出现数据不一致? 答案要点:fail-fast 只是检测并发结构性修改,不保证完整检测(例如某些非结构修改或恰好未触发检查),并且在无同步时读取到的元素可能是半写入状态或旧值。

练习 5(源码理解)

ensureCapacityInternal(size + 1) 为什么要传 size+1 而不是 size答案要点:因为要确保至少能容纳新增的那个元素;minCapacity = size+1

练习 6(实操)

实现一个 trimToSize() 的用途? 答案要点:把 elementData 缩小到 size,释放空余内存,适合大批量删除后回收内存。


六、背诵模板(直接背会话)

下面三段你直接背:面试问到就背诵即可(每段控制在 2~3 句)

模板 A(简介)

ArrayList 底层是动态数组,用 Object[] 存储元素,支持基于索引的 O(1) 访问。插入/删除中间元素需要移动数组,最坏 O(n)。扩容时会申请更大数组并复制旧数据,常见扩容因子是 1.5 倍。

模板 B(线程与并发)

ArrayList 不是线程安全的;多线程写入会导致竞态和不确定行为。若需要并发访问可以用 Collections.synchronizedList(外部加锁)或 CopyOnWriteArrayList(读多写少)。

模板 C(fail-fast)

Iterator 使用 modCount 校验并发修改,发现不一致会抛 ConcurrentModificationException(fail-fast)。这只是检测机制,不是并发安全方案。


七、记忆小技巧 / 快速口语化解释(面试口语友好)

  • 把“扩容策略 1.5 倍”记成“省内存又少复制”。
  • modCount 想像成“版本号”,iterator 带着它的 expectedVersion,发现版本变了就报警。
  • 随机访问快 = “像数组下标直达”,插入慢 = “像搬椅子要把后面人都往后挤”。

八、按时间的复习计划(可选:选择 30 / 60 / 120 分钟)

30 分钟(冲刺面试)
  • 0–5min:读“底层地图”并默写(第一节)。
  • 5–15min:背三个“模板 A/B/C”。
  • 15–25min:看伪码(add/grow/remove/iterator),口头演示一次扩容与一次删除移动。
  • 25–30min:做练习题 1、2(口头回答)。
60 分钟(稳妥)
  • 0–10:底层结构与术语(size/capacity/modCount)理解。
  • 10–25:看并手写伪码(add/grow/remove)。
  • 25–40:背模板 + 白板画图演示。
  • 40–55:做全部练习题,写出精简答案。
  • 55–60:复习记忆小技巧与面试回答演练。
120 分钟(深挖)
  • 按 60 分钟计划做,再加:
    • 看 ArrayList 的真实源码(对应 JDK 版本)并理解 ensureCapacityInternal 的细节。
    • 比较 ArrayListVector/CopyOnWriteArrayList 的源码差异。
    • 做两道 LeetCode(与数组相关的插入/删除类题)来巩固思路。

九、常见追问及应对(面试官常跟进的问题,如何回答)

  1. 为什么 ArrayList 的 remove 返回被删除元素? 简短:便于链式/确认删除前值,和 Collection 接口契约一致。
  2. 为什么 elementData[size] = null 为了避免内存泄漏,帮助 GC 回收对象引用。
  3. 什么时候使用 CopyOnWriteArrayList? 读多写少且迭代要求不抛 CME 时(例如事件监听器列表)。
  4. 如何避免频繁扩容? 在构造器里传入合适的初始容量或在批量插入前调用 ensureCapacity

十、快速总结(面试前一页纸)

把下面这段记下来,作为你口袋里的“一页纸”:

ArrayList = 动态数组(Object[]),随机访问 O(1),中间插入/删除 O(n)。扩容触发于 size + 1 > capacity,常见扩容为 old + old>>1(1.5 倍)。非线程安全;通过 modCount 实现 fail-fast。常用替代:LinkedList(频繁插入删除)、CopyOnWriteArrayList(读多写少并发场景)、Collections.synchronizedList(外部同步)。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-01-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、先把“底层地图”记住(必须能画在白板上)
  • 二、关键方法与伪源码(只看这几段就够问答)
    • 1) add(E e)
    • 2) grow()
    • 3) remove(int index)
    • 4) Iterator (fail-fast 检查)
  • 三、面试高频问答与标准答案(可直接背)
    • Q1:ArrayList 底层是什么?优缺点?
    • Q2:扩容机制(什么时候扩容、扩多大、为什么 1.5 倍)?
    • Q3:为什么不是线程安全?怎么保证线程安全?
    • Q4:fail-fast 是如何实现的?有什么局限?
    • Q5:增删操作的时间复杂度?
  • 四、白板演示要点(面试展示技巧)
  • 五、真题/练习题(练习后对答案)
    • 练习 1(短答)
    • 练习 2(代码推演)
    • 练习 3(复杂度)
    • 练习 4(并发)
    • 练习 5(源码理解)
    • 练习 6(实操)
  • 六、背诵模板(直接背会话)
  • 七、记忆小技巧 / 快速口语化解释(面试口语友好)
  • 八、按时间的复习计划(可选:选择 30 / 60 / 120 分钟)
    • 30 分钟(冲刺面试)
    • 60 分钟(稳妥)
    • 120 分钟(深挖)
  • 九、常见追问及应对(面试官常跟进的问题,如何回答)
  • 十、快速总结(面试前一页纸)
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档