当前位置: 首页 > news >正文

ArrayList vs LinkedList:底层原理与实战选择指南

ArrayList vs LinkedList:底层原理与实战选择指南

概述

ArrayList 和 LinkedList 是 Java 集合框架中最常用的两个 List 实现类,看似功能相似(都可存储有序、可重复的元素),但底层结构截然不同,导致性能差异巨大。

本文将从底层实现核心方法原理时间复杂度三个维度拆解两者的区别,帮你彻底搞懂 “为什么 ArrayList 查找快,LinkedList 插入删除快”,以及如何根据场景选择。

一、底层结构:数组 vs 双向链表

1. ArrayList:动态数组

底层基于连续的数组实现,所有元素在内存中占用连续空间,通过索引(下标)直接访问。

可以理解为 “一排连续的抽屉”,每个抽屉有编号(索引),能直接找到第 n 个抽屉:

索引:0   1   2   3   ...  
元素:A → B → C → D → ...(内存地址连续)  

关键特性

  • 数组容量固定,ArrayList 会在容量不足时自动扩容(默认扩容为原容量的 1.5 倍)。

  • 必须预留一定的空闲空间(避免频繁扩容),可能造成内存浪费。

2. LinkedList:双向链表

底层基于双向链表实现,元素(Node 节点)在内存中分散存储,每个节点包含:

  • 数据域(存储元素值)

  • 前驱指针(prev):指向前一个节点

  • 后继指针(next):指向后一个节点

可以理解为 “串起来的珠子”,每个珠子记得前一个和后一个珠子的位置:

Node1       Node2       Node3  
┌───┐       ┌───┐       ┌───┐  
│ A │◄─────►│ B │◄─────►│ C │  
└───┘       └───┘       └───┘  
(prev=null)  (prev=Node1)  (prev=Node2)  
(next=Node2) (next=Node3)  (next=null)  

关键特性

  • 元素无需连续存储,内存利用率更高(按需分配节点)。

  • 节点间通过指针关联,访问元素需从头部 / 尾部遍历。

二、核心方法原理:为什么性能差异大?

1. 查找元素(get (int index))

ArrayList:直接定位(O (1))

基于数组的随机访问特性,通过索引直接计算内存地址:

// ArrayList.get() 核心源码
public E get(int index) {rangeCheck(index); // 检查索引是否越界return elementData(index); // 直接返回数组中index位置的元素
}// 数组访问:O(1)时间复杂度
E elementData(int index) {return (E) elementData[index];
}

举例:要找第 3 个元素,直接访问数组下标 2(索引从 0 开始),一步到位。

LinkedList:遍历查找(O (n))

链表没有索引,必须从头部(或尾部)逐个遍历节点:

// LinkedList.get() 核心源码
public E get(int index) {checkElementIndex(index);return node(index).item; // 先找到节点,再返回数据
}// 查找节点:需遍历
Node<E> node(int index) {// 优化:如果索引在前半段,从头部遍历;否则从尾部遍历if (index < (size >> 1)) { // 前半段Node<E> x = first;for (int i = 0; i < index; i++)x = x.next; // 从头往后找return x;} else { // 后半段Node<E> x = last;for (int i = size - 1; i > index; i--)x = x.prev; // 从后往前找return x;}
}

举例:要找第 1000 个元素,需从头部开始,逐个移动指针 999 次,效率随元素数量增加而下降。

2. 插入元素(add (int index, E element))

ArrayList:可能需要移动元素(O (n))
  • 如果插入位置在末尾:直接添加(O (1),但需考虑扩容耗时)。

  • 如果插入位置在中间:需移动插入点后的所有元素,腾出位置:

// ArrayList.add() 核心源码(插入中间)
public void add(int index, E element) {rangeCheckForAdd(index);ensureCapacityInternal(size + 1); // 确保容量足够// 复制数组:将index后的元素向后移动1位(耗时操作)System.arraycopy(elementData, index, elementData, index + 1, size - index);elementData[index] = element; // 插入新元素size++;
}

举例:在容量为 1000 的数组中,向第 500 位插入元素,需移动 500 个元素,效率低。

LinkedList:只需修改指针(O (1))

无论插入位置在哪,只需修改相邻节点的指针,无需移动其他元素:

// LinkedList.add() 核心源码(插入中间)
public void add(int index, E element) {checkPositionIndex(index);if (index == size) // 插入末尾linkLast(element);else // 插入中间linkBefore(element, node(index)); // 先找到目标节点,再修改指针
}// 插入节点到succ之前
void linkBefore(E e, Node<E> succ) {final Node<E> pred = succ.prev;final Node<E> newNode = new Node<>(pred, e, succ);succ.prev = newNode; // 后节点的prev指向新节点if (pred == null) // 如果是头节点first = newNode;elsepred.next = newNode; // 前节点的next指向新节点size++;
}

举例:在第 1000 个节点前插入新节点,只需修改第 999 个和第 1000 个节点的指针,两步完成。

3. 删除元素(remove (int index))

ArrayList:需移动元素(O (n))

删除中间元素后,需将后续元素向前移动,填补空缺:

// ArrayList.remove() 核心源码
public E remove(int index) {rangeCheck(index);modCount++;E oldValue = elementData(index);int numMoved = size - index - 1;if (numMoved > 0)// 移动元素:将index后的元素向前移动1位System.arraycopy(elementData, index + 1, elementData, index, numMoved);elementData[--size] = null; // 清空最后一位,帮助GCreturn oldValue;
}
LinkedList:修改指针(O (1))

找到目标节点后,只需断开其与前后节点的关联:

// LinkedList.remove() 核心源码
public E remove(int index) {checkElementIndex(index);return unlink(node(index)); // 找到节点后,断开链接
}// 断开节点链接
E unlink(Node<E> x) {final E element = x.item;final Node<E> next = x.next;final Node<E> prev = x.prev;if (prev == null) { // 头节点first = next;} else {prev.next = next; // 前节点的next指向后节点x.prev = null;}if (next == null) { // 尾节点last = prev;} else {next.prev = prev; // 后节点的prev指向前节点x.next = null;}x.item = null; // 清空数据,帮助GCsize--;return element;
}

三、时间复杂度对比表

操作ArrayListLinkedList性能差异原因
查找(get)O (1)(随机访问)O (n)(遍历)ArrayList 直接用索引,LinkedList 需遍历
末尾插入(add)O (1)(无扩容时)O(1)两者效率相近
中间插入(add)O (n)(移动元素)O (1)(改指针)ArrayList 需移动元素,LinkedList 仅改指针
中间删除(remove)O (n)(移动元素)O (1)(改指针)同插入逻辑
遍历(迭代器)O(n)O(n)遍历次数相同,ArrayList 缓存友好略快

四、使用场景选择策略

  1. 优先选 ArrayList 的场景
    • 频繁查询(get 操作多),如展示列表、数据检索。
    • 元素数量固定或变化不大(避免频繁扩容)。
    • 内存充足,可接受一定的空间浪费。
  1. 优先选 LinkedList 的场景
    • 频繁在中间插入 / 删除(如实现队列、栈、链表结构)。
    • 元素数量动态变化大,且内存紧张(无需预留空间)。
  1. 特殊注意
    • 即使选 LinkedList,也应避免通过索引(get (index))频繁访问元素(O (n) 效率低),建议用迭代器遍历。
    • ArrayList 的扩容会消耗额外时间(复制数组),可初始化时指定容量(new ArrayList<>(1000))优化。

总结:核心要点速览

对比维度ArrayListLinkedList
底层结构动态数组(连续内存)双向链表(分散内存)
核心优势查找快(O (1))插入 / 删除快(中间位置 O (1))
内存特性需预留空间,可能浪费按需分配,内存利用率高
适用场景读多写少写多(中间插入 / 删除)读少

记住:数组适合查,链表适合改,根据操作频率选择,而非盲目跟风。实际开发中,ArrayList 因查询效率高,使用场景更广泛,但在队列、栈等场景中,LinkedList 是更好的选择。

http://www.dtcms.com/a/344452.html

相关文章:

  • 企业设备系统选型:功能适配度分析
  • Java多线程面试题二
  • 视频清晰度:静态码率比动态码率更优秀吗?
  • 从零搭建 React 工程化项目
  • 本地通过跳板机连接无公网IP的内网服务器
  • 哈尔滨云前沿服务器托管的优势
  • 【Linux仓库】进程的“夺舍”与“飞升”:exec 驱动的应用现代化部署流水线
  • 前端github-workflows部署腾讯云轻量服务器
  • 学云计算还是网络,选哪个好?
  • Linux:网络层IP协议
  • alicloud 阿里云有哪些日志 审计日志
  • css的white-space: pre
  • Docker 命令大全
  • VsCode 上的Opencv(C++)环境配置(Linux)
  • 四种方法把 Proxy 对象代理数组处理成普通数组
  • URP+Unistorm5.3.0 -> webGL天空黑屏的处理
  • 如何精准高效地比对两份合同的差异?
  • Java数据结构——7.2 二叉树-二叉树
  • MPLS原理
  • 新能源知识库(84)什么是IEC白皮书
  • 初识数据结构——Map和Set:哈希表与二叉搜索树的魔法对决
  • CoreShop微信小程序商城框架开启多租户-添加一个WPF客户端以便进行本地操作--读取店铺信息(6)
  • 循环神经网络实战:GRU 对比 LSTM 的中文情感分析(三)
  • UE5关卡蓝图能不能保存副本呀?
  • Pandas 合并数据集:concat 和 append
  • 2025年城市建设与公共管理国际会议(UCPM 2025)
  • Linux之Ubuntu入门:Vmware中虚拟机中的Ubuntu中的shell命令-常用命令
  • C++实现简易线程池:理解 function 与 bind 的妙用
  • CMake进阶:Ninja环境搭建与加速项目构建
  • JVM-(8)JVM启动的常用命令以及参数