ArrayList 插入慢,LinkedList 一定快吗

「Java 进阶之路」系列 Day21

写在前面

模块二收尾了,从这篇开始进入模块三------集合框架。ArrayListLinkedList 是最常被拿来对比的一对,很多人张口就是"数组适合查、链表适合改",但一问到"LinkedList插入一定比ArrayList快吗",就容易想岔。这篇从底层结构讲起,把这个常见误解纠正过来。


一、是什么:两种完全不同的底层结构

flowchart TB subgraph s1[ArrayList底层] arr[一段连续的Object数组<br/>下标0 下标1 下标2 下标3] end subgraph s2[LinkedList底层] n0[头节点] --> n1[节点] n1 --> n2[节点] n2 --> n3[尾节点] n1 --> n0 n2 --> n1 n3 --> n2 end

ArrayList 内部是一段连续的数组Object[] elementData),每个元素紧挨着存放,靠下标直接定位;LinkedList 是一条双向链表,每个节点各自存着数据、以及指向前一个/后一个节点的引用,元素在内存里是分散的,靠一个个跳指针才能找到目标节点。


二、为什么会有性能差异:结构决定了访问方式

数组因为是连续内存,知道下标就能直接算出内存地址,一步到位;链表没有这种"按下标计算地址"的能力,想找到第 N 个节点,只能从头(或从尾,LinkedList 会根据下标离哪端近选择方向)一个个往后跳,跳 N 次才能到。这个结构性差异决定了两者操作的时间复杂度全面不同:

操作 ArrayList LinkedList
按下标随机访问 get(index) O(1) 直接计算地址 O(n) 要从头或尾跳指针过去
在头部插入删除 O(n) 后面所有元素要整体后移 O(1) 只改头节点的指针
在尾部插入删除 O(1) 通常直接追加,除非要扩容 O(1) 只改尾节点的指针
在中间插入删除 O(n) 定位是O(1),但要移动后续元素 O(n) 移动指针是O(1),但定位到这个位置要遍历
内存占用 较低,只存数据本身 较高,每个节点还要多存两个指针引用

表格里最容易被忽略的一行是"中间插入删除" ------很多人只记住"链表插入删除是O(1)",却忘了这个O(1)指的是"改指针"这一步本身,但在中间位置插入删除之前,你必须先找到这个位置,而链表定位到中间某个下标同样得老老实实一个个跳过去,是O(n)。所以严格来说,在一个已知下标的中间位置插入删除,ArrayList和LinkedList的总耗时都是O(n),只是O(n)里具体在忙活的事情不一样------一个在搬元素,一个在跳指针。

真正能让LinkedList发挥O(1)优势的场景,是已经拿着一个指向目标位置的Iterator,直接在当前位置insert/remove ,不需要重新从头定位------这也是为什么LinkedList实现的ListIterator支持add/remove操作,是它相比ArrayList真正的优势场景,而不是"随便传一个下标进去插入就一定快"。


三、怎么用:ArrayList 的扩容机制

ArrayList 默认初始容量是 10,当元素个数超过当前数组容量时会触发扩容,源码里的核心逻辑是:

java 复制代码
// 简化后的ArrayList扩容逻辑
private Object[] grow() {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);   // 新容量约等于旧容量的1.5倍
    elementData = Arrays.copyOf(elementData, newCapacity); // 拷贝到一个新数组
    return elementData;
}

扩容的代价是一次完整的数组拷贝 ,容量从10到15、从15到22,每次扩容都要把旧数组的内容整个复制到新数组里,这也是O(n)的操作。如果能提前预估数据量,用带初始容量的构造方法 new ArrayList<>(1000),能省掉扩容过程中反复拷贝的开销。

LinkedList完全没有这个问题 ------它不需要连续内存,来一个元素就 new 一个节点挂上去,没有"容量不够、整体搬家"这种烦恼,但代价是每个节点都要额外存两个引用(prevnext),元素少的时候这个开销占比不明显,元素一多,内存占用会明显高于 ArrayList


四、实际选型:LinkedList 用得比想象中少

现代 Java 开发里,LinkedList 实际使用频率远低于很多人的想象。原因是:

  • 绝大多数业务场景是"遍历读取"或者"尾部追加",这两种操作 ArrayList 都是强项
  • 需要频繁在两端进出(当队列/栈用)的场景,ArrayList 家族里的 ArrayDeque 通常性能更好------ArrayDeque 底层是循环数组,没有 LinkedList 每个节点额外存指针的内存开销,Java 官方文档也建议优先用 ArrayDeque 而不是 LinkedList 来实现栈或队列
  • LinkedList 真正的用武之地,是那种"拿着迭代器边遍历边在当前位置插入删除"的场景,这在实际业务代码里并不常见

五、面试追问

Q1:ArrayList 和 LinkedList 的核心区别是什么?

底层数据结构不同:ArrayList 是连续的动态数组,靠下标直接计算地址访问;LinkedList 是双向链表,靠节点间的指针一个个跳转访问。这个结构差异决定了两者在随机访问、插入删除等操作上的时间复杂度完全不同。

Q2:在集合中间位置插入一个元素,LinkedList 真的比 ArrayList 快吗?

不一定。如果只是传入一个下标去插入,LinkedList 定位到这个下标同样需要从头或尾遍历,是O(n),插入本身改指针才是O(1),两者加起来还是O(n),和 ArrayList 需要搬移后续元素的O(n)本质上是一个量级,只是具体耗时在做的事情不同。LinkedList 真正的O(1)优势,只体现在已经持有指向目标位置的Iterator、直接在当前位置操作时。

Q3:ArrayList 的扩容机制是怎样的?

默认初始容量是10,元素个数超过当前数组长度时触发扩容,新容量大约是旧容量的1.5倍(oldCapacity + oldCapacity >> 1),然后把旧数组的全部元素拷贝到新数组里。这个拷贝过程是O(n)的开销,如果能预估数据规模,用带初始容量的构造方法能减少扩容次数。

Q4:为什么说 LinkedList 在实际项目中用得比想象中少?

因为大部分业务场景是遍历读取或者尾部追加,这些操作 ArrayList 效率更高;即使是需要频繁两端进出的队列/栈场景,ArrayDeque 通常比 LinkedList 性能更好、内存占用更低(LinkedList 每个节点要多存两个指针引用)。LinkedList 真正适合的场景------边遍历边在当前位置插入删除------在实际业务代码里并不多见。

Q5:LinkedList 比 ArrayList 多占用的内存开销体现在哪?

LinkedList 的每个节点除了存储数据本身,还要额外存两个引用字段分别指向前一个和后一个节点;而 ArrayList 的数组里每个位置只存数据本身,没有这类额外的指针开销。数据量越大,LinkedList 这部分额外的内存占用就越明显。


下一篇预告

Day22 讲 HashMap 的底层原理------数组加链表加红黑树的混合结构,扩容机制是怎么运作的,这是集合框架里面试问法最深的一个。

相关推荐
用户3126874877201 小时前
ReentrantLock 到底怎么排队的?AQS 源码拆解
java
花生智源1 小时前
RAG检索优化:查询改写、重排序与缓存策略
后端
吃饱了得干活1 小时前
从类爆炸到协作——DDD战略设计登场
java·后端·架构
程序员cxuan1 小时前
我用 DeepSeek-V4-Pro,完美复刻了苹果官网
人工智能·后端·程序员
wno7041 小时前
Spring Boot异常处理
java·spring boot·后端
君顾12 小时前
智慧场馆解决方案小程序系统开发实战指南
java·开发语言·智慧场馆
AI多Agent协作实战派2 小时前
AI多Agent协作系统实战(四十五):漏声明了一个变量,整个页面的按钮都死了
后端
学习星球2 小时前
【LeetCode算法题精讲】二分查找精讲
java·数据结构·算法·leetcode·职场和发展·图搜索
Escalating_xu2 小时前
【Linux线程同步】从数据竞争到 mutex、条件变量与生产者消费者(上篇)
android·java·linux