面试题 1:ArrayList 的底层数据结构是什么?扩容机制是怎样的?
很多人只会说一句:
"ArrayList 底层是数组。"
但在面试中,这远远不够。
一、底层结构:动态数组(Object[])
JDK 8 中的核心字段:
java
transient Object[] elementData;
private int size;
特点:
- 使用 连续内存 存储元素
- 支持 随机访问 (
get(index)时间复杂度 O(1)) - 插入/删除中间元素时,需要移动后续元素,时间复杂度 O(n)
二、扩容机制:1.5 倍扩容
核心逻辑(简化):
java
int newCapacity = oldCapacity + (oldCapacity >> 1); // 旧容量 + 旧容量的一半
也就是说,容量增长大约为 1.5 倍:
- 10 → 15 → 22 → 33 → 49 → ...
扩容过程大致为:
- 创建一个更大容量的新数组
- 使用
System.arraycopy把原数组内容拷贝过去 - 用新数组替换旧数组的引用
三、为什么不是 2 倍扩容?
- 2 倍扩容速度快,但内存浪费严重
- 1.5 倍是在 减少扩容次数 与 控制内存占用 之间做了折中
- 扩容本身是一个"重操作",但并不是每次 add 都扩容,而是个别情况下触发
面试官追问
ArrayList什么时候触发扩容?- 如果明确知道要放多少数据,应该怎么写可以减少扩容?
- 为什么说频繁扩容会影响性能?
易错点
- 以为
ArrayList每次扩容都是 +1 或 +固定值(错误) - 以为扩容是"逻辑操作",不涉及数组复制(错误)
- 完全不知道扩容比例是 1.5 倍
面试题 2:LinkedList 的底层结构是什么?为什么随机访问很慢?
很多人只记得:
"LinkedList 是链表。"
但真正要回答"好",你需要说明:什么样的链表 + 有什么代价。
一、底层结构:双向链表(Doubly Linked List)
源码结构(简化):
java
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}
每个节点包含:
- 前驱指针
prev - 后继指针
next - 数据
item
整个表由 first 和 last 引用头尾。
二、为什么随机访问慢?
访问第 i 个元素时:
- 判断是靠近头还是靠近尾
- 从
first或last一步步往中间走 - 时间复杂度 O(n)
与之相反,ArrayList 可以通过 elementData[index] 直接定位,时间复杂度 O(1)。
三、LinkedList 的优势是什么?
- 插入/删除已知节点本身的操作是 O(1)
- 适合频繁在头尾插入/删除(
addFirst/removeFirst)
注意:中间插入/删除要先遍历找到位置,本质还是 O(n)。
面试官追问
- LinkedList 的插入/删除为什么是 O(1)?
- 在实际业务中,LinkedList 是否真的常用?
- 如果需要实现队列/栈结构,你会选 ArrayList 还是 LinkedList?
易错点
- 错误地认为 "LinkedList 插入/删除都是 O(1)" ------ 忽略了"先找到节点"的成本
- 忽略了链表对内存不友好(节点分散,不利于 CPU Cache)
面试题 3:ArrayList 和 LinkedList 在增删查改性能上的差异?
这题经常以"开卷题"的形式问你:
什么时候选 ArrayList,什么时候选 LinkedList?
一、复杂度对比表
| 操作 | ArrayList | LinkedList | 哪个更适合 |
|---|---|---|---|
| 按下标随机访问(get) | O(1) | O(n) | ArrayList |
| 尾部插入(addLast) | 均摊 O(1) | O(1) | 差不多 |
| 中间插入/删除(已知下标) | O(n) | 找节点 O(n) + 操作 O(1) | 差不多 |
| 头部插入/删除 | O(n)(需要搬移) | O(1) | LinkedList |
二、正确的理解方式
-
查得多,用 ArrayList
- 比如大量
for遍历、按index访问
- 比如大量
-
改得多(特别是头尾)、链型队列,用 LinkedList
- 比如队列/双端队列结构
-
实际项目中:
- 绝大部分场景下,ArrayList 使用远多于 LinkedList
面试官追问
-
遍历谁快?为什么?
一般是 ArrayList 更快,因为内存连续,CPU 预取效果好
-
LRU 缓存会选哪个数据结构?
-
有没有同时兼顾两者优点的结构?(例如:数组 + 链表混合结构)
易错点
- 只会背时间复杂度,不知道真实业务如何选择
- 以为 LinkedList 天生"适合大量插入删除",忽略"找位置"的成本
- 不知道 CPU Cache 对 ArrayList 的性能提升有多大
面试题 4:什么是 fail-fast 机制?为什么遍历 ArrayList 时会抛 ConcurrentModificationException?
这题是集合框架中非常经典的面试题。
一、什么是 fail-fast?
当在遍历集合的过程中,集合结构被"非法修改"(非迭代器方式),迭代器会立刻抛出
ConcurrentModificationException。
比如:
java
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
for (String s : list) {
if ("B".equals(s)) {
list.remove(s); // 这里会触发 ConcurrentModificationException
}
}
二、底层实现:modCount 机制
ArrayList 中有一个 结构修改计数器:
java
protected transient int modCount = 0;
- 每次结构性修改(add / remove / clear / 扩容)都会
modCount++ - 迭代器中会保存一个
expectedModCount初始值 - 每次遍历前会判断:
java
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
如果在遍历中使用 list.remove() 等方法修改了集合:
- modCount 变了
- expectedModCount 未改变
- 两者不一致,直接抛异常
三、为什么要有 fail-fast?
- 让开发者 尽早发现错误使用 集合的方式
- 避免悄悄产生数据错乱、难以排查的问题
- 不用于保证线程安全,而是用于暴露问题
面试官追问
-
ConcurrentModificationException 一定是多线程问题吗?
答:不是,单线程也会触发(如上述例子)
-
Hashtable / ConcurrentHashMap 是否会 fail-fast?
-
如何在遍历时安全地删除元素?
易错点
- 误以为 "ConcurrentModificationException = 并发问题"
- 不知道 modCount 的存在
- 认为这是"JVM 的 bug" 😂
面试题 5:为什么 Iterator.remove() 不会触发 fail-fast?正确删除方式是什么?
一、错误删除方式(会触发 fail-fast)
java
for (String s : list) {
if ("B".equals(s)) {
list.remove(s); // 触发 ConcurrentModificationException
}
}
因为:
list.remove()修改了 modCount- 但是 expectedModCount 仍然是旧值
- 下次迭代时检查不一致 → 抛异常
二、正确删除方式:使用 Iterator.remove()
java
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if ("B".equals(s)) {
it.remove(); // 正确做法
}
}
在 Iterator.remove() 方法内部,会:
- 检查 modCount 与 expectedModCount 是否一致
- 调用集合自身的删除逻辑
- 同步更新 expectedModCount,使其与 modCount 保持一致
因此不会触发 fail-fast。
面试官追问
- for-each 底层其实是用 Iterator 吗?
- ListIterator 与 Iterator 的区别是什么?
- 为什么说 Iterator.remove() 是唯一安全的遍历删除方式?
易错点
- 在 for-each 中调用 list.remove() / list.add()
- 使用普通 for 循环删除元素导致下标混乱
- 不理解为什么 Iterator.remove() 是"特权方法"
本篇总结
本篇我们围绕 List 体系,深入解析了:
- ArrayList 的底层结构与 1.5 倍扩容机制
- LinkedList 的双向链表实现与随机访问性能瓶颈
- ArrayList 与 LinkedList 在不同操作下的性能差异与应用场景
- fail-fast 机制:modCount / expectedModCount 的检查原理
- 为什么 Iterator.remove() 是遍历删除元素的唯一正确姿势
这些内容是 Java 集合框架面试中的高频考点,也是从"会用 API"走向"理解底层原理"的关键一环。