从源码看ArrayList与LinkedList的性能差异

从源码看ArrayList与LinkedList的性能差异

从源码深挖ArrayList与LinkedList的性能差异

在Java集合框架中,ArrayList和LinkedList作为List接口的两个典型实现,经常被拿来对比。很多开发者仅停留在“ArrayList基于数组、LinkedList基于链表”的表层认知,却忽略了底层源码设计对性能的决定性影响。本文将从源码出发,从初始化、增删改查、内存占用等多维度拆解两者的性能差异,帮你在实际开发中精准选型。


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

要理解性能差异,首先得从两者的底层数据结构说起,这是一切性能表现的根源。

ArrayList:动态扩容的Object数组

ArrayList的核心是一个transient Object[] elementData数组,Java源码中定义如下:

Java

复制

/** * The array buffer into which the elements of the ArrayList are stored. * The capacity of the ArrayList is the length of this array buffer. */ transient Object[] elementData; // non-private to simplify nested class access

数组的特性是内存连续存储,这让随机访问可以通过下标直接定位,时间复杂度为O(1);但也带来了扩容成本和插入删除时的元素移动开销。

LinkedList:带首尾指针的双向链表

LinkedList的底层是双向链表结构,每个节点用Node类封装:

Java

复制

private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }

同时维护了首尾节点指针:

Java

复制

transient Node<E> first; transient Node<E> last;

链表的节点在内存中分散存储,通过指针关联,插入删除仅需改变指针指向,但随机访问需要遍历链表,时间复杂度为O(n)。


⚡ 核心操作性能对比

接下来我们从源码角度,逐一分析增、删、改、查四大核心操作的性能差异。

🔍 查找:ArrayList完胜,O(1) vs O(n)

ArrayList的随机访问: 直接通过数组下标定位,源码非常简洁:

Java

复制

public E get(int index) { Objects.checkIndex(index, size); return elementData(index); } E elementData(int index) { return (E) elementData[index]; }

只要下标合法,就能在常数时间内获取元素,这是数组天生的优势。

LinkedList的随机访问: 需要从首节点或尾节点开始遍历,源码如下:

Java

复制

public E get(int index) { checkElementIndex(index); return node(index).item; } Node<E> node(int index) { // assert isElementIndex(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; } }

虽然做了优化(判断index靠近头部还是尾部,减少遍历次数),但最坏情况下仍需遍历半个链表,时间复杂度为O(n)。

结论:频繁随机访问场景,ArrayList性能碾压LinkedList。


➕ 新增:头部/中间插入LinkedList优,尾部插入ArrayList更稳

新增操作的性能差异,主要体现在是否需要移动元素扩容成本

ArrayList新增元素

  • 尾部新增:如果数组未扩容,直接赋值,时间复杂度O(1);如果需要扩容,会触发grow()方法,将原数组复制到新数组,时间复杂度变为O(n)。

Java

复制

public boolean add(E e) { modCount++; add(e, elementData, size); return true; } private void add(E e, Object[] elementData, int s) { if (s == elementData.length) elementData = grow(); elementData[s] = e; size = s + 1; }

  • 中间/头部新增:需要将插入位置后的所有元素向后移动一位,时间复杂度O(n),源码如下:

Java

复制

public void add(int index, E element) { rangeCheckForAdd(index); modCount++; final int s; Object[] elementData; if ((s = size) == (elementData = this.elementData).length) elementData = grow(); System.arraycopy(elementData, index, elementData, index + 1, s - index); elementData[index] = element; size = s + 1; }

LinkedList新增元素

  • 尾部新增:直接将新节点设为last,原last的next指向新节点,时间复杂度O(1)

Java

复制

public boolean add(E e) { linkLast(e); return true; } void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; }

  • 中间/头部新增:只需找到对应节点,改变前后指针指向,无需移动元素,时间复杂度O(1)(但查找插入位置需要O(n)时间)

Java

复制

public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); } void linkBefore(E e, Node<E> succ) { // assert succ != null; final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode; if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; }

结论

  1. 频繁在列表头部/中间插入元素,LinkedList更优(前提是已找到插入位置);
  2. 仅在尾部新增元素,ArrayList性能更稳定(无扩容时O(1),扩容时O(n))。

🗑️ 删除:逻辑与新增对称,LinkedList仍有优势

删除操作的性能逻辑和新增类似,核心差异还是元素移动 vs 指针修改

ArrayList删除元素

  • 尾部删除:直接将对应位置置为null,时间复杂度O(1);
  • 中间/头部删除:需要将删除位置后的元素向前移动一位,时间复杂度O(n),源码如下:

Java

复制

public E remove(int index) { Objects.checkIndex(index, size); final Object[] es = elementData; @SuppressWarnings("unchecked") E oldValue = (E) es[index]; fastRemove(es, index); return oldValue; } private void fastRemove(Object[] es, int i) { modCount++; final int newSize; if ((newSize = size - 1) > i) System.arraycopy(es, i + 1, es, i, newSize - i); es[size = newSize] = null; // clear to let GC do its work }

LinkedList删除元素: 只需改变对应节点的前后指针,时间复杂度O(1)(同样,查找待删除节点需要O(n)时间):

Java

复制

public E remove(int index) { checkElementIndex(index); return unlink(node(index)); } E unlink(Node<E> x) { // assert x != null; 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; x.prev = null; } if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } x.item = null; // help GC size--; modCount++; return element; }

结论:删除操作的性能差异与新增一致,LinkedList在非尾部删除时更有优势。


✏️ 修改:ArrayList O(1),LinkedList O(n)

修改操作的核心是定位元素 + 赋值

  • ArrayList直接通过下标定位元素,赋值即可,时间复杂度O(1);
  • LinkedList需要先遍历找到对应节点,再修改item值,时间复杂度O(n)。

ArrayList修改源码:

Java

复制

public E set(int index, E element) { Objects.checkIndex(index, size); E oldValue = elementData(index); elementData[index] = element; return oldValue; }

LinkedList修改源码:

Java

复制

public E set(int index, E element) { checkElementIndex(index); Node<E> x = node(index); E oldVal = x.item; x.item = element; return oldVal; }

结论:修改操作ArrayList性能远优于LinkedList。


💾 内存占用:ArrayList更紧凑,LinkedList节点开销大

内存占用也是选型的重要参考,尤其是在存储大量数据时。

ArrayList:连续内存,浪费可控

插入广告:各行各业学习千款源码就上:svipm.com.cn

ArrayList的内存开销主要是数组本身的占用 + 少量扩容预留空间。数组是连续存储的,没有额外指针开销,内存利用率高。即使扩容时会预留一些空间(默认扩容为原容量的1.5倍),但这个浪费是可控的。

LinkedList:每个节点额外存储两个指针

LinkedList的每个节点都要存储prevnext两个指针,对于存储大量小对象(如Integer、Short等)的场景,指针的内存开销甚至会超过数据本身。例如存储1000个Integer对象,ArrayList仅需存储1000个对象引用,而LinkedList需要存储1000个对象引用 + 2000个指针,内存开销明显更大。


🎯 选型建议:场景优先,拒绝教条

看完以上分析,我们可以结合实际场景给出选型建议:

优先选择ArrayList的场景
  1. 频繁进行随机访问(get/set操作);
  2. 数据量较大,对内存占用敏感;
  3. 元素主要在尾部新增/删除,或新增删除操作较少;
  4. 需要与数组进行频繁转换(ArrayList的toArray()方法效率很高)。
优先选择LinkedList的场景
  1. 频繁在列表头部或中间进行新增/删除操作;
  2. 不需要随机访问,仅需按顺序遍历(迭代器遍历两者性能相近);
  3. 数据量较小,且需要频繁调整元素位置(如队列、栈场景,LinkedList可直接实现Deque接口)。

📝 总结:底层结构决定性能上限

ArrayList和LinkedList的性能差异,本质是数组和双向链表两种数据结构的特性差异。数组的连续存储带来了高效的随机访问,但也有扩容和元素移动的开销;链表的分散存储让增删操作更灵活,但随机访问效率低下,且内存开销更大。

在实际开发中,不要迷信“ArrayList比LinkedList快”的教条,而是要结合具体场景的核心操作(是随机访问多还是增删多)、数据量大小等因素综合判断。希望本文的源码分析能帮你打破认知误区,在Java集合选型上做到胸有成竹。

Read more

2026年 WordPress 极速优化指南:从服务器底层到前端缓存

2026年 WordPress 极速优化指南:从服务器底层到前端缓存

说明:以下内容皆由本人在叹惋博客试验完成。   原文链接:2026年 WordPress 极速优化指南:从服务器底层到前端缓存-叹惋博客   前言: 做网站最痛苦的不是没有流量,而是有流量但网站卡顿。我的博客托管在香港 CN2 线路(无备案),受限于跨境物理延迟和昂贵的带宽,速度优化一直是我的心病。 最近,我将服务器从 1核2G 升级到了 2核4G。采用了雨云-新一代云服务提供商的服务器 硬件翻倍只是基础,为了榨干这台机器的性能,我在软件层面进行了一场“外科手术式”的深度调优。从编译 Nginx Brotli 模块到挂载 Tmpfs 内存盘,从 MySQL 8.4 救砖调优到 PHP JIT,我几乎触碰到了 WordPress 性能的物理天花板。 本文将详细记录我实施的全套优化方案,每一个步骤都是踩坑后的经验总结。 第一章:服务器内核与环境的“核动力”改造 在触碰

By Ne0inhk

MogFace人脸检测模型-WebUI开源可部署:MIT许可证+完整训练/推理代码公开

MogFace人脸检测模型-WebUI开源可部署:MIT许可证+完整训练/推理代码公开 想不想拥有一个能精准识别各种人脸的AI助手?无论是光线昏暗的角落、戴着口罩的侧脸,还是拥挤人群中的模糊面孔,它都能一眼找出。今天要介绍的MogFace人脸检测模型,不仅精度高、速度快,更重要的是它完全开源,附带完整的WebUI界面,让你在10分钟内就能搭建起自己的专业人脸检测服务。 1. 为什么选择MogFace? 在开始动手之前,我们先聊聊为什么MogFace值得你花时间。市面上的人脸检测工具不少,但MogFace有几个硬核优势,让它脱颖而出。 1.1 开源自由,没有后顾之忧 MogFace采用MIT许可证开源,这意味着你可以: * 免费商用:无论是个人项目还是商业应用,都不用担心授权费用 * 随意修改:源代码完全开放,你可以根据需求定制功能 * 自由分发:可以集成到自己的产品中,无需额外授权 很多AI模型要么闭源,要么有复杂的商业限制。MogFace的MIT许可证给了开发者最大的自由度,这也是它能在GitHub上获得高星关注的重要原因。 1.2 精度高,稳定性好 Mog

By Ne0inhk

Clawdbot镜像免配置部署Qwen3-32B:Web网关直连方案快速上手指南

Clawdbot镜像免配置部署Qwen3-32B:Web网关直连方案快速上手指南 1. 为什么你需要这个方案 你是不是也遇到过这样的情况:想试试最新发布的Qwen3-32B大模型,但一打开部署文档就看到密密麻麻的环境依赖、CUDA版本校验、模型分片加载、API服务配置……光是看就头大?更别说还要自己搭Web界面、处理跨域、调试端口转发了。 Clawdbot镜像就是为解决这个问题而生的。它不是另一个需要你从零编译、反复调试的项目,而是一个“开箱即用”的完整推理平台——把Qwen3-32B直接封装进预置镜像里,连Ollama服务、模型加载、Web网关、前端交互全给你配好了。你只需要启动它,打开浏览器,就能和320亿参数的大模型对话。 这不是概念演示,也不是简化版demo。它背后跑的是原生Qwen3-32B权重,通过Ollama标准API接入,再经由Clawdbot内置代理将8080端口无缝映射到18789网关,全程无需修改配置文件、无需安装额外组件、无需理解反向代理原理。对开发者来说,省下的是两小时部署时间;对业务方来说,换来的是当天就能试跑真实场景的响应速度。 下面我们就从零

By Ne0inhk

【前端小站】HTML 标签:网页骨架,从空白到惊艳,全靠这些 HTML 标签搞事情

【前端小站】HTML 标签:网页骨架,从空白到惊艳,全靠这些 HTML 标签搞事情 HTML 标签就像积木,不同的组合方式决定了你能搭出什么样的房子。 下面这张图是目前(2025–2026)最常用、最有语义价值、实际项目中出现频率最高的标签分类速览表: 层级主要职责核心标签(按重要性排序)2025–2026 高频使用场景语义分值(SEO/无障碍)文档根声明 + 语言 + 根元素<!DOCTYPE html><html lang="zh-CN">几乎所有页面★★★★★元信息不显示但极其重要<meta charset="UTF-8"><meta name=

By Ne0inhk