第 3 篇:ArrayList / LinkedList / fail-fast 深度解析(5 题)

面试题 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 → ...

扩容过程大致为:

  1. 创建一个更大容量的新数组
  2. 使用 System.arraycopy 把原数组内容拷贝过去
  3. 用新数组替换旧数组的引用

三、为什么不是 2 倍扩容?

  1. 2 倍扩容速度快,但内存浪费严重
  2. 1.5 倍是在 减少扩容次数控制内存占用 之间做了折中
  3. 扩容本身是一个"重操作",但并不是每次 add 都扩容,而是个别情况下触发

面试官追问

  1. ArrayList 什么时候触发扩容?
  2. 如果明确知道要放多少数据,应该怎么写可以减少扩容?
  3. 为什么说频繁扩容会影响性能?

易错点

  • 以为 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

整个表由 firstlast 引用头尾。


二、为什么随机访问慢?

访问第 i 个元素时:

  1. 判断是靠近头还是靠近尾
  2. firstlast 一步步往中间走
  3. 时间复杂度 O(n)

与之相反,ArrayList 可以通过 elementData[index] 直接定位,时间复杂度 O(1)。


三、LinkedList 的优势是什么?

  • 插入/删除已知节点本身的操作是 O(1)
  • 适合频繁在头尾插入/删除(addFirst / removeFirst

注意:中间插入/删除要先遍历找到位置,本质还是 O(n)。


面试官追问

  1. LinkedList 的插入/删除为什么是 O(1)?
  2. 在实际业务中,LinkedList 是否真的常用?
  3. 如果需要实现队列/栈结构,你会选 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

二、正确的理解方式

  1. 查得多,用 ArrayList

    • 比如大量 for 遍历、按 index 访问
  2. 改得多(特别是头尾)、链型队列,用 LinkedList

    • 比如队列/双端队列结构
  3. 实际项目中:

    • 绝大部分场景下,ArrayList 使用远多于 LinkedList

面试官追问

  1. 遍历谁快?为什么?

    一般是 ArrayList 更快,因为内存连续,CPU 预取效果好

  2. LRU 缓存会选哪个数据结构?

  3. 有没有同时兼顾两者优点的结构?(例如:数组 + 链表混合结构)


易错点

  • 只会背时间复杂度,不知道真实业务如何选择
  • 以为 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?

  • 让开发者 尽早发现错误使用 集合的方式
  • 避免悄悄产生数据错乱、难以排查的问题
  • 不用于保证线程安全,而是用于暴露问题

面试官追问

  1. ConcurrentModificationException 一定是多线程问题吗?

    答:不是,单线程也会触发(如上述例子)

  2. Hashtable / ConcurrentHashMap 是否会 fail-fast?

  3. 如何在遍历时安全地删除元素?


易错点

  • 误以为 "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() 方法内部,会:

  1. 检查 modCount 与 expectedModCount 是否一致
  2. 调用集合自身的删除逻辑
  3. 同步更新 expectedModCount,使其与 modCount 保持一致

因此不会触发 fail-fast。


面试官追问

  1. for-each 底层其实是用 Iterator 吗?
  2. ListIterator 与 Iterator 的区别是什么?
  3. 为什么说 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"走向"理解底层原理"的关键一环。

相关推荐
T___T3 小时前
class 出现前,JS 是怎么继承的
前端·javascript·面试
天天扭码4 小时前
京东前端开发实习生 一面
前端·网络协议·面试
诗和远方14939562327345 小时前
动态库和静态库的区别
面试
WYiQIU8 小时前
突破字节前端2-1⾯试: JS异步编程问题应答范式及进阶(视频教学及完整源码笔记)
开发语言·前端·javascript·vue.js·笔记·面试·github
byc8 小时前
Android 存储目录<内部存储,外部存储app专属,外部存储公共>
android·面试
长安er8 小时前
LeetCode 11盛最多水的容器 & LeetCode 42接雨水-双指针2
面试·力扣·双指针·接雨水
a努力。8 小时前
网易Java面试被问:fail-safe和fail-fast
java·windows·后端·面试·架构
踏浪无痕9 小时前
MySQL 脏读、不可重复读、幻读?一张表+3个例子彻底讲清!
后端·面试·架构
豆苗学前端9 小时前
彻底讲透浏览器的事件循环,吊打面试官
前端·javascript·面试