📚 前言
在Java开发中,ArrayList
和LinkedList
是List
接口下最常用的两个实现类,几乎贯穿所有业务场景。但很多开发者只知其"用",不知其"理"------比如为什么频繁插入删除用LinkedList
?为什么ArrayList
随机访问更快?两者底层结构到底有什么差异?
本文将从数据结构基础出发,拆解两者的底层实现、核心特性、性能差异,结合实战案例和面试高频考点,帮你彻底搞懂"什么时候该用哪个",避开常见坑点。
一、先搞懂基础:线性表的两种存储结构
要理解ArrayList
和LinkedList
,必须先掌握它们的"共同祖先"------线性表的两种核心存储方式,这是所有差异的根源。
1.1 线性表的定义
线性表是n个相同类型数据元素的有限序列,逻辑上呈"连续直线"结构,比如数组、链表都属于线性表。
1.2 两种存储结构对比(关键!)
特性 | 顺序存储(数组) | 链式存储(链表) |
---|---|---|
物理地址 | 连续内存空间 | 离散内存空间(节点通过指针连接) |
访问方式 | 下标随机访问,O(1) | 遍历指针访问,O(n) |
插入/删除效率 | 需移动后续元素,O(n) | 仅修改指针,O(1)(找到节点后) |
空间利用率 | 可能浪费(扩容后未用空间) | 按需分配,无空间浪费 |
典型实现 | ArrayList | LinkedList |
一句话总结:数组查得快,链表改得快------这是后续所有结论的核心依据。
二、ArrayList:动态数组的实现与核心特性
ArrayList
本质是"可自动扩容的数组",解决了固定数组容量不够用的问题,是日常开发的"首选集合"。
2.1 底层结构拆解(源码简化)
java
public class ArrayList<E> {
// 存储元素的底层数组
transient Object[] elementData;
// 集合中有效元素个数(≠数组容量)
private int size;
// 默认初始容量(无参构造首次add时触发)
private static final int DEFAULT_CAPACITY = 10;
}
2.2 常用API实战(附代码)
1. 构造方法(3种常用)
java
// 1. 无参构造(懒加载:初始为空数组,首次add才扩容到10)
List<Integer> list1 = new ArrayList<>();
// 2. 指定初始容量(推荐!已知元素数量时,避免频繁扩容)
List<Integer> list2 = new ArrayList<>(20); // 初始容量20
// 3. 基于其他集合构造(快速拷贝元素)
List<String> list3 = new ArrayList<>(Arrays.asList("Java", "Spring"));
2. 核心操作(增删查改)
java
List<Integer> list = new ArrayList<>();
// 增:尾插(默认)、指定位置插
list.add(1); // 尾插,O(1)(无扩容时)
list.add(0, 0); // 指定下标0插入,后续元素后移,O(n)
// 删:按下标删、按元素删
list.remove(1); // 删下标1的元素,后续元素前移,O(n)
list.remove(Integer.valueOf(0)); // 删值为0的元素,O(n)(需先找位置)
// 查:按下标查、判断包含
int val = list.get(0); // 下标查,O(1)(核心优势)
boolean hasVal = list.contains(1); // 遍历查找,O(n)
// 改:按下标改
list.set(0, 100); // O(1)
3. 遍历方式(推荐优先级)
遍历方式 | 代码示例 | 适用场景 |
---|---|---|
增强for循环 | for (Integer num : list) { ... } |
仅遍历,代码简洁 |
for循环+下标 | for (int i=0; i<list.size(); i++) |
需要下标(如修改元素) |
迭代器 | Iterator<Integer> it = list.iterator() |
边遍历边删除(避免并发异常) |
2.3 灵魂特性:动态扩容机制
这是ArrayList
最核心的设计,也是面试高频考点!
扩容触发条件
当执行add
时,若size + 1 > elementData.length
(数组满了),则触发扩容。
扩容流程(源码核心逻辑)
- 计算新容量 :默认按
1.5倍
扩容(newCapacity = oldCapacity + (oldCapacity >> 1)
,位运算比乘法快); - 修正容量上限 :若新容量超过
Integer.MAX_VALUE
,则取Integer.MAX_VALUE
(避免内存溢出); - 数组拷贝 :通过
Arrays.copyOf(elementData, newCapacity)
创建新数组,拷贝原元素(耗时操作,O(n))。
避坑点
- 频繁扩容会严重影响性能!如果知道元素数量,一定要用"指定初始容量"的构造方法;
- 扩容后的空数组会浪费内存(比如容量100满了扩到150,只存105个元素,浪费45个空间)。
三、LinkedList:双向链表的实现与核心特性
LinkedList
基于无头双向循环链表实现,核心优势是"插入删除灵活",还能当栈/队列用。
3.1 底层结构拆解(源码简化)
链表的基本单元是"节点",每个节点存储元素和前后指针:
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.prev = prev;
this.next = next;
}
}
public class LinkedList<E> {
Node<E> first; // 首节点(头指针)
Node<E> last; // 尾节点(尾指针)
int size; // 有效元素个数
}
3.2 常用API实战(附代码)
1. 构造方法(2种)
java
// 1. 无参构造(初始为空链表)
List<String> list1 = new LinkedList<>();
// 2. 基于其他集合构造
List<Integer> list2 = new LinkedList<>(Arrays.asList(1, 2, 3));
2. 核心操作(增删查改)
java
LinkedList<Integer> list = new LinkedList<>();
// 增:头插、尾插(核心优势,O(1))
list.addFirst(0); // 头插,直接改first指针
list.addLast(2); // 尾插,直接改last指针
list.add(1, 1); // 中间插,需先找位置(O(n)),再改指针(O(1))
// 删:头删、尾删(O(1))
list.removeFirst(); // 头删
list.removeLast(); // 尾删
list.remove(0); // 中间删,需先找位置(O(n))
// 查:按下标查(坑点!O(n))
int val = list.get(0); // 需从first遍历到下标0,效率低
3. 特殊用法:当栈/队列
LinkedList
实现了Deque
接口,可直接作为栈或队列使用:
java
// 1. 栈(后进先出)
LinkedList<Integer> stack = new LinkedList<>();
stack.push(1); // 入栈(头插)
stack.pop(); // 出栈(头删)
// 2. 队列(先进先出)
LinkedList<Integer> queue = new LinkedList<>();
queue.offer(1); // 入队(尾插)
queue.poll(); // 出队(头删)
4. 遍历方式(避坑!)
- 推荐:增强for循环、迭代器(支持反向遍历);
- 绝对禁止 :for循环+下标(每次
get(index)
都要从头遍历,时间复杂度O(n²),性能爆炸差)!
java
//反向迭代器
ListIterator<Integer> rit = list.listIterator(list.size());
while (rit.hasPrevious()) {
System.out.println(rit.previous()); // 从尾到头遍历
}
四、ArrayList vs LinkedList:核心差异对比(面试必问)
对比维度 | ArrayList | LinkedList |
---|---|---|
底层结构 | 动态数组(连续空间) | 无头双向循环链表(离散) |
随机访问(get) | 支持,O(1)(核心优势) | 不支持,O(n) |
头插/头删 | O(n)(需移动所有元素) | O(1)(仅改指针) |
尾插/尾删 | O(1)(无扩容时) | O(1) |
中间插入/删除 | O(n)(移动元素) | O(n)(找位置耗时) |
容量特性 | 需扩容(1.5倍) | 无容量概念,节点按需创建 |
内存占用 | 可能浪费(扩容空空间) | 节点存前后指针,开销略大 |
缓存友好性 | 好(连续内存,命中高) | 差(离散内存,命中低) |
4.1 适用场景总结(一句话选对!)
- 用
ArrayList
:频繁随机访问(查多改少)、元素数量稳定; - 用
LinkedList
:频繁头尾插入/删除、需实现栈/队列; - 不确定时:优先用
ArrayList
(日常开发中"查"比"改"更频繁)。
五、实战案例:从理论到实践
5.1 案例1:ArrayList实现扑克牌洗牌发牌
5.2 案例2:LinkedList实现消息队列
六、面试高频考点与避坑指南
6.1 高频面试题(附标准答案)
1. ArrayList默认容量是10吗?为什么?
答:是,但无参构造时初始为空数组 (DEFAULTCAPACITY_EMPTY_ELEMENTDATA
),首次执行add
时才扩容到10(懒加载,避免初始占用内存)。
2. ArrayList扩容倍数为什么是1.5倍,不是2倍?
答:1.5倍能平衡"扩容频率"和"内存浪费":
- 2倍扩容可能导致后续扩容时,旧数组无法被复用(比如容量100扩到200,只用101个元素,浪费99个,且旧数组占内存);
- 1.5倍扩容更易触发内存回收,减少碎片。
3. LinkedList是循环链表吗?怎么证明?
答:是无头双向循环链表 。源码中last.next = first
且first.prev = last
,首尾节点相连,形成环形结构。
4. 为什么遍历LinkedList不能用for循环+下标?
答:每次get(index)
都要从首/尾节点遍历到目标位置,n次遍历的时间复杂度是O(n²),性能极差。
5. 如何让ArrayList线程安全?
答:有两种方案:
- 用
Collections.synchronizedList(list)
包装(底层加同步锁,性能一般); - 用
CopyOnWriteArrayList
(写时复制,读多写少场景性能更优,适合日志收集等场景)。
6.2 避坑指南
- ArrayList坑点:频繁在中间插入删除(用LinkedList替代)、不指定初始容量导致频繁扩容;
- LinkedList坑点:用下标遍历(改用迭代器)、随机访问(用ArrayList替代);
- 通用坑点:多线程直接用(需加锁或用线程安全集合)。
七、总结
ArrayList
和LinkedList
没有"谁更好",只有"谁更合适":
- ArrayList:数组实现,查得快,适合"查多改少";
- LinkedList:链表实现,改得快(头尾),适合"改多查少"或实现栈/队列。
掌握底层结构→理解性能差异→匹配业务场景,这才是正确使用集合的核心逻辑。
如果本文对你有帮助,欢迎点赞+收藏+关注!有疑问或补充,欢迎在评论区交流~
关注我,后续分享更多Java核心知识点和实战技巧!