【Java基础】- 集合 - ArrayList与LinkedList

ArrayList

一、底层核心结构

  • 底层数据结构:动态数组 transient Object[] elementData
  • 默认容量:DEFAULT_CAPACITY = 10
  • 最大容量:Integer.MAX_VALUE - 8
  • 关键变量
    • elementData:存储元素的数组缓冲区
    • size:实际元素个数(不是数组长度)
    • modCount:结构修改次数,用于迭代器fail-fast

二、添加元素 & 扩容机制

1. add (E e) 流程

  1. 检查是否需要扩容(ensureCapacityInternal)
  2. 数组末尾赋值 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 / 栈 / 队列 / 双端队列 使用

三、查询原理

  1. 检查下标是否越界
  2. 判断 index 在前半段还是后半段
    • 小于 size/2 → 从头遍历
    • 大于 size/2 → 从尾遍历
  3. 逐个节点移动,直到找到目标

时间复杂度:O (n):即使二分遍历,最坏仍要走一半,依然很慢

四、增删原理

增删快是指:只改变节点引用,不移动其他元素

  1. 头部插入 / 删除
    • 直接修改 first 节点的 prev 和 next
    • 时间复杂度:O(1)
  2. 尾部插入 / 删除
    • 直接修改 last 节点
    • 时间复杂度:O(1)
  3. 中间插入 / 删除
    • 先找到目标位置(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 满足多种需求。
二、底层实现原理
  1. 循环数组设计

    • 核心思想:将数组首尾相连形成闭环,任何一点都可能被看作起点或终点。
    • 关键指针
      • head:指向队列头部第一个有效元素。
      • tail:指向队列尾部下一个可插入位置。
    • 容量约束 :数组长度始终为 2 的幂次方,便于位运算优化。
  2. 高效扩容机制

    • 扩容时机 :当 head == tail 时(表示数组已满)。
    • 扩容策略 :容量翻倍(newCapacity = n << 1)。
    • 扩容过程
      1. 计算 head 到数组末端的元素数量 r = n - p
      2. 复制 head 到末端的元素到新数组头部。
      3. 复制剩余元素到新数组后续位置。
  3. 位运算优化

    • 关键技巧 :使用 (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%
四、典型应用场景
  1. 算法题与数据结构

    • DFS/BFS 的标配:作为队列实现广度优先搜索。
    • 滑动窗口(Sliding Window):高效地在两端添加/删除元素。
    • 撤销操作:作为历史记录栈使用。
    • 单调栈问题 :比 Stack 类更高效。
  2. 电商系统实战应用

    • 订单处理队列:单线程环境下处理订单的高效队列。
    • 购物车操作 :当需要频繁在头部/尾部操作时(比 LinkedList 更优)。
    • 商品推荐队列:维护用户实时推荐商品的滑动窗口。
    • 用户行为记录:高效记录用户浏览、点击等行为序列。
五、使用指南与最佳实践
  1. 常用 API

    • 作为栈使用(LIFO)

      java 复制代码
      Deque<Integer> stack = new ArrayDeque<>();
      stack.push(1);      // 入栈
      int top = stack.pop(); // 出栈
    • 作为队列使用(FIFO)

      java 复制代码
      Deque<Integer> queue = new ArrayDeque<>();
      queue.offer(1);     // 入队
      int head = queue.poll(); // 出队
  2. 注意事项

    • 避免使用 addFirst/addLast :失败会抛 IllegalStateException,应优先用 offerFirst/offerLast(失败返回 false)。
    • 多线程环境 :不能直接使用,需用 ConcurrentLinkedDeque 或显式锁。
    • null 元素 :插入 null 会直接抛 NullPointerException
    • 初始化容量 :明确知道元素规模时指定初始容量(如 new ArrayDeque(1024)),避免多次扩容。
  3. 性能优化技巧

    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
  • 用作栈时ArrayDequeStack 更快且更现代。
  • 用作队列时ArrayDequeLinkedList 性能更好。

最佳实践 :在电商系统和算法题中,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更优)
    • 适用于:订单处理队列、消息队列等
相关推荐
程序员萌萌2 分钟前
Java之mysql实战讲解(三):聚簇索引与非聚簇索引
java·mysql·聚簇索引
tankeven8 分钟前
动态规划专题(03):区间动态规划从原理到实践(未完待续)
c++·算法·动态规划
好家伙VCC16 分钟前
**发散创新:基于Python与ROS的机器人运动控制实战解析**在现代机器人系统开发中,**运动控制**是实现智能行为的核心
java·开发语言·python·机器人
2401_8274999916 分钟前
python项目实战09-AI智能伴侣(ai_partner_2-3)
开发语言·python
派葛穆18 分钟前
汇川PLC-Python与汇川easy521plc进行Modbustcp通讯
开发语言·python
代码小书生1 小时前
Matplotlib,Python 数据可视化核心库!
python·信息可视化·matplotlib
程途知微1 小时前
ConcurrentHashMap线程安全实现原理全解析
java·后端
Mars酱1 小时前
1分钟编写贪吃蛇 | JSnake贪吃蛇单机版
java·后端·开源
devpotato1 小时前
人工智能(四)- Function Calling 核心原理与实战
java·人工智能
田梓燊1 小时前
2026/4/11 leetcode 3741
数据结构·算法·leetcode