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;
}
三、时间复杂度对比表
操作 | ArrayList | LinkedList | 性能差异原因 |
---|---|---|---|
查找(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 缓存友好略快 |
四、使用场景选择策略
- 优先选 ArrayList 的场景:
-
- 频繁查询(get 操作多),如展示列表、数据检索。
-
- 元素数量固定或变化不大(避免频繁扩容)。
-
- 内存充足,可接受一定的空间浪费。
- 优先选 LinkedList 的场景:
-
- 频繁在中间插入 / 删除(如实现队列、栈、链表结构)。
-
- 元素数量动态变化大,且内存紧张(无需预留空间)。
- 特殊注意:
-
- 即使选 LinkedList,也应避免通过索引(get (index))频繁访问元素(O (n) 效率低),建议用迭代器遍历。
-
- ArrayList 的扩容会消耗额外时间(复制数组),可初始化时指定容量(new ArrayList<>(1000))优化。
总结:核心要点速览
对比维度 | ArrayList | LinkedList |
---|---|---|
底层结构 | 动态数组(连续内存) | 双向链表(分散内存) |
核心优势 | 查找快(O (1)) | 插入 / 删除快(中间位置 O (1)) |
内存特性 | 需预留空间,可能浪费 | 按需分配,内存利用率高 |
适用场景 | 读多写少 | 写多(中间插入 / 删除)读少 |
记住:数组适合查,链表适合改,根据操作频率选择,而非盲目跟风。实际开发中,ArrayList 因查询效率高,使用场景更广泛,但在队列、栈等场景中,LinkedList 是更好的选择。