ArrayList
一、底层核心结构
- 底层数据结构:动态数组 transient Object[] elementData
- 默认容量:DEFAULT_CAPACITY = 10
- 最大容量:Integer.MAX_VALUE - 8
- 关键变量
- elementData:存储元素的数组缓冲区
- size:实际元素个数(不是数组长度)
- modCount:结构修改次数,用于迭代器fail-fast
二、添加元素 & 扩容机制
1. add (E e) 流程
- 检查是否需要扩容(ensureCapacityInternal)
- 数组末尾赋值 elementData[size++] = e
2. 扩容规则(核心)
- 新容量 = 旧容量 + 旧容量 >> 1 → 1.5 倍扩容
- 底层调用 Arrays.copyOf(elementData, newCapacity) 复制数组
- 若指定了初始容量,按指定大小初始化
- 超过 MAX_ARRAY_SIZE 时直接用 Integer.MAX_VALUE
3. 扩容流程
添加元素 → 容量不足 → 1.5倍扩容 → 复制数组 → 插入元素
三、查询、删除原理
1. 查询 get (int index)
- 直接下标访问:elementData[index]
- 时间复杂度 O(1),随机访问极快
2. 删除 remove (int index)
- 检查下标合法性
- 计算需要移动的元素个数
- 调用 System.arraycopy 整体前移覆盖(效率低 O(n))
- size--,置空末尾引用帮助 GC
四、操作特点
| 操作类型 | 效率 | 原因说明 |
|---|---|---|
| 随机查询(get) | O(1) 极快 | 底层为数组,连续内存,可通过索引直接定位元素,无需遍历 |
| 尾部插入 | O(1) 快 | 直接在数组末尾赋值,不移动其他元素,不扩容时效率极高 |
| 尾部删除 | O(1) 快 | 只需 size-- 并置空末尾元素,无需移动其他元素 |
| 中间/头部插入 | O(n) 慢 | 插入位置后的所有元素需整体向后移动,涉及数组拷贝 |
| 中间/头部删除 | O(n) 慢 | 删除位置后的所有元素需整体向前移动,涉及数组拷贝 |
| 扩容操作 | O(n) 慢 | 创建新数组,复制原数组全部元素,开销较大 |
五、fail-fast 机制
- 每次 add/remove 都会让 modCount++
- 迭代器遍历会比对 modCount
- 不一致 → 立即抛出 ConcurrentModificationException
安全删除方式:
- iterator.remove()
- list.removeIf(...)
六、线程不安全问题
- 没有任何锁
- 多线程并发 add 可能出现:
- 数据覆盖、丢失
- 数组下标越界
高并发解决方案:
- Collections.synchronizedList
- CopyOnWriteArrayList(推荐)
七、优缺点
优点
- 随机访问极快(数组特性)
- 内存紧凑,无额外指针开销
- 尾插效率高
- 实现简单、通用
缺点
- 中间插入 / 删除效率低
- 扩容有复制开销
- 线程不安全
八、总结
- ArrayList 底层是动态数组,懒加载初始容量 10,1.5 倍扩容,查询 O (1) 极快,中间增删 O (n) 慢,尾部增删接近O(1)
- 非线程安全,并发场景需用同步包装或 CopyOnWriteArrayList
- 迭代器 fail-fast,大数据量建议指定初始容量
LinkedList
一、底层核心结构
- 底层数据结构:双向链表
- JDK1.6 及以前:双向循环链表
- JDK1.7+:标准双向链表(非循环)
- 核心节点类 Node(内部静态类)
- 三个关键属性:
- Node first 头节点
- Node last 尾节点
- int size 元素个数
- modCount 结构修改次数(fail-fast)
二、LinkedList 特点
- 有序、可重复、允许 null
- 无容量限制,不需要扩容
- 不支持随机访问,查询慢
- 头尾增删极快
- 线程不安全
- 实现了 List + Deque,可当 List / 栈 / 队列 / 双端队列 使用
三、查询原理
- 检查下标是否越界
- 判断 index 在前半段还是后半段
- 小于 size/2 → 从头遍历
- 大于 size/2 → 从尾遍历
- 逐个节点移动,直到找到目标
时间复杂度:O (n):即使二分遍历,最坏仍要走一半,依然很慢
四、增删原理
增删快是指:只改变节点引用,不移动其他元素
- 头部插入 / 删除
- 直接修改 first 节点的 prev 和 next
- 时间复杂度:O(1)
- 尾部插入 / 删除
- 直接修改 last 节点
- 时间复杂度:O(1)
- 中间插入 / 删除
- 先找到目标位置(O (n))
- 再修改前后节点指针(O (1))
综合:O(n),但指针修改开销极小
五、LinkedList 效率总结
| 操作 | 效率 | 说明 |
|---|---|---|
| 随机访问 get(int index) | O(n) | 必须从头/尾遍历查找 |
| 头部增删 | O(1) | 直接改指针,无需遍历 |
| 尾部增删 | O(1) | 直接改指针,无需遍历 |
| 中间增删 | O(n) | 查找位置O(n),修改指针O(1) |
| 扩容 | 无需扩容 | 链表天然动态扩展 |
六、优缺点
| 维度 | 优点 | 缺点 |
|---|---|---|
| 增删效率 | 头尾增删效率极高,为 O(1) | 中间增删需要先遍历查找,效率一般 |
| 查询效率 | 无明显优势 | 不支持随机访问,查询需遍历,时间复杂度 O(n) |
| 内存占用 | 按需分配节点,无扩容浪费 | 每个节点需保存 prev/next 指针,内存开销更大 |
| 扩容机制 | 天然支持动态扩展,无需扩容 | 无扩容问题,但节点分散,CPU 缓存命中率低 |
| 功能特性 | 实现 Deque 接口,可做栈、队列、双端队列 | 功能多但日常场景下性能不如 ArrayList |
七、总结
LinkedList 是双向链表,无扩容、头尾增删 O (1),查询 O (n) 慢;内存开销更大、缓存不友好,适合队列 / 栈,不适合频繁随机访问
ArrayDeque
ArrayDeque:高性能双端队列的终极指南
ArrayDeque(Array Double Ended Queue)是 Java 集合框架中基于循环数组 实现的高性能双端队列。它兼具栈和队列的功能,比 LinkedList 更快、比 Stack 更安全,是算法题和电商系统中栈/队列场景的首选实现。
一、核心特性与优势
- 定义 :Java 6 引入,位于
java.util包中。 - 继承关系 :
public class ArrayDeque<E> extends AbstractCollection<E> implements Deque<E>, Cloneable, Serializable - 核心优势 :
- 比 LinkedList 更快:基于数组,CPU 缓存命中率高。
- 比 Stack 更安全 :
Stack是过时的遗留类,ArrayDeque是现代替代品。 - 内存紧凑 :比
LinkedList节省约 50% 内存(无节点指针开销)。
- 关键特性 :
- 无容量限制:自动扩容(类似 ArrayList),最小容量为 8。
- 非线程安全:追求极致性能(多线程需外部同步)。
- 拒绝 null 元素 :避免歧义(
poll()返回 null 表示空)。 - 支持栈和队列操作:一套 API 满足多种需求。
二、底层实现原理
-
循环数组设计
- 核心思想:将数组首尾相连形成闭环,任何一点都可能被看作起点或终点。
- 关键指针 :
head:指向队列头部第一个有效元素。tail:指向队列尾部下一个可插入位置。
- 容量约束 :数组长度始终为 2 的幂次方,便于位运算优化。
-
高效扩容机制
- 扩容时机 :当
head == tail时(表示数组已满)。 - 扩容策略 :容量翻倍(
newCapacity = n << 1)。 - 扩容过程 :
- 计算
head到数组末端的元素数量r = n - p。 - 复制
head到末端的元素到新数组头部。 - 复制剩余元素到新数组后续位置。
- 计算
- 扩容时机 :当
-
位运算优化
- 关键技巧 :使用
(head - 1) & (elements.length - 1)代替取余操作。 - 原理 :因数组长度为 2 的幂,
elements.length - 1的二进制位全为 1,与操作相当于取余。 - 优势:处理负数情况更高效,避免了传统取余的性能开销。
- 关键技巧 :使用
三、ArrayDeque vs LinkedList
| 特性 | ArrayDeque | LinkedList |
|---|---|---|
| 底层结构 | 动态数组(循环数组实现) | 双向链表 |
| 内存占用 | 连续内存,节省空间 | 每个节点需额外存储指针,内存占用更大 |
| 随机访问 | O(1)(直接按索引访问) | O(n)(必须遍历链表) |
| 头尾插入/删除 | O(1)(但可能触发扩容) | O(1)(不需扩容) |
| 中间插入/删除 | O(n)(需移动元素) | O(1)(只需改指针,但需先遍历找到位置) |
| 缓存命中 | 高(数组连续存储,CPU 缓存友好) | 低(链表节点分散,可能导致 cache miss) |
| 扩容问题 | 容量不够时自动扩容 2 倍 | 不需要扩容 |
| 线程安全 | 非线程安全 | 非线程安全 |
性能对比数据(2023 年测试):
| 操作 | ArrayDeque | LinkedList | 优势幅度 |
|---|---|---|---|
| 头部插入 | O(1) | O(1) | 快 40% |
| 尾部插入 | O(1) | O(1) | 快 35% |
| 随机访问 | O(n) | O(n) | 快 10 倍 |
| 内存占用 | 更少 | 更多 | 节省 50% |
四、典型应用场景
-
算法题与数据结构
- DFS/BFS 的标配:作为队列实现广度优先搜索。
- 滑动窗口(Sliding Window):高效地在两端添加/删除元素。
- 撤销操作:作为历史记录栈使用。
- 单调栈问题 :比
Stack类更高效。
-
电商系统实战应用
- 订单处理队列:单线程环境下处理订单的高效队列。
- 购物车操作 :当需要频繁在头部/尾部操作时(比
LinkedList更优)。 - 商品推荐队列:维护用户实时推荐商品的滑动窗口。
- 用户行为记录:高效记录用户浏览、点击等行为序列。
五、使用指南与最佳实践
-
常用 API
-
作为栈使用(LIFO) :
javaDeque<Integer> stack = new ArrayDeque<>(); stack.push(1); // 入栈 int top = stack.pop(); // 出栈 -
作为队列使用(FIFO) :
javaDeque<Integer> queue = new ArrayDeque<>(); queue.offer(1); // 入队 int head = queue.poll(); // 出队
-
-
注意事项
- 避免使用
addFirst/addLast:失败会抛IllegalStateException,应优先用offerFirst/offerLast(失败返回false)。 - 多线程环境 :不能直接使用,需用
ConcurrentLinkedDeque或显式锁。 - null 元素 :插入
null会直接抛NullPointerException。 - 初始化容量 :明确知道元素规模时指定初始容量(如
new ArrayDeque(1024)),避免多次扩容。
- 避免使用
-
性能优化技巧
java// 预分配容量(避免 4-5 次扩容) Deque<Integer> deque = new ArrayDeque<>(1024); // 优先使用 offer/poll 而非 add/remove(避免异常开销) if (deque.offerLast(1)) { // 处理成功 } else { // 处理失败(扩容失败等情况) } // 清空队列时使用 clear() 而非循环 remove deque.clear();
六、总结与建议
ArrayDeque 是 Java 集合框架中算法优化的典范,通过循环数组的精妙设计和位运算优化,实现了极致的性能表现。
- 除非需要
LinkedList的特定功能 (如中间插入删除),否则应优先选择ArrayDeque。 - 用作栈时 :
ArrayDeque比Stack更快且更现代。 - 用作队列时 :
ArrayDeque比LinkedList性能更好。
最佳实践 :在电商系统和算法题中,90% 的栈/队列场景应优先使用 ArrayDeque。
三者对比
| 数据结构 | 核心特性 | 电商业务应用场景 |
|---|---|---|
| ArrayList | 1. 基于动态数组,内存连续 2. 随机访问快(O(1)) 3. 插入/删除慢(O(n),需移动元素) 4. 适合读多写少场景 | 1. 商品列表展示:快速查询和展示商品信息 2. 用户收藏夹:需要频繁读取收藏商品 3. 订单历史查询:用户查看历史订单记录 |
| LinkedList | 1. 基于双向链表,内存不连续 2. 插入/删除快(O(1),只需修改指针) 3. 随机访问慢(O(n),需遍历) 4. 适合写多读少场景 | 1. 订单处理流程:频繁插入新订单和删除已完成订单 2. 购物车操作:频繁增删商品 3. 消息队列:处理订单状态变更通知 |
| ArrayQueue | 1. 基于循环数组,实现 FIFO 队列 2. 入队/出队快(O(1)) 3. 不支持随机访问 4. 适合任务排队场景 | 1. 秒杀/抢购队列:排队处理用户请求,保证先到先得 2. 支付回调处理:按顺序处理支付结果 3. 物流状态更新:按顺序处理物流信息 |
应用场景
【电商】商品目录与展示数据
| 应用场景 | 推荐数据结构 | 核心原因 | 性能对比 |
|---|---|---|---|
| 商品列表展示 | ArrayList | 高频随机访问需求,需快速定位特定位置商品 | 随机访问:ArrayList 2ms vs LinkedList 650ms |
| 促销活动商品列表 | ArrayList | 需要快速排序、筛选和分页展示 | 遍历性能:ArrayList 15ms vs LinkedList 18ms |
| 推荐商品列表 | ArrayList | 以读取为主,需快速定位和个性化展示 | 缓存友好性:ArrayList连续内存布局提升CPU缓存命中率 |
| 商品详情页关联商品 | ArrayList | 内存敏感场景,访问频繁,修改较少 | 内存占用:ArrayList比LinkedList少30%-35% |
【电商】购物车与收藏
| 应用场景 | 推荐数据结构 | 核心原因 | 性能对比 |
|---|---|---|---|
| 购物车操作 | LinkedList | 频繁在任意位置插入/删除商品,无需随机访问 | 中间插入:ArrayList 420ms vs LinkedList 8ms |
| 用户收藏夹 | LinkedList | 用户频繁添加/删除收藏,位置变化大 | 删除操作:ArrayList O(n) vs LinkedList O(1) |
| 用户浏览历史 | LinkedList | 持续在末尾添加记录,偶尔中间删除 | 尾部插入:ArrayList 3ms vs LinkedList 5ms |
选择原则
- 查询操作占比>30% → 优先选择ArrayList
- 适用于:商品目录、缓存数据、用户信息查询等
- 中间插入/删除占比>50% → 优先选择LinkedList
- 适用于:购物车、订单队列、用户行为记录等
- 仅头部/尾部操作 → 考虑ArrayDeque(比LinkedList更优)
- 适用于:订单处理队列、消息队列等