👉 用 ArrayList
存数据,结果插入时卡住了?
👉 想删除某个元素,却发现索引错乱了?
👉 不知道该用 ArrayList
还是 LinkedList
,选错了导致性能瓶颈?
一、List
是什么?—— 有序可重复的“列表”
🎯 核心特点
- 有序(Ordered):元素按插入顺序排列
- 允许重复:可以存储多个相同的值
- 支持索引访问:可以通过
get(index)
快速定位元素
✅ 典型场景:
- 用户列表展示
- 购物车商品管理
- 日志记录
🖼️ 图示:List
的逻辑结构
+--------+ +--------+ +--------+
| index0 | -> | index1 | -> | index2 | -> ...
+--------+ +--------+ +--------+
| valueA | | valueB | | valueC |
+--------+ +--------+ +--------+
✅ 核心操作:
- 添加元素:
add(E e)
- 获取元素:
get(int index)
- 删除元素:
remove(int index)
二、List
的核心实现类
1. ArrayList
—— 基于“动态数组”的实现
🎯 核心特性
- 底层是数组:
Object[] elementData
- 支持快速随机访问:通过索引直接定位,时间复杂度
O(1)
- 动态扩容:当容量不够时,自动扩展(默认扩容 1.5 倍)
- 非线程安全
List<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
String first = list.get(0); // 快速获取第一个元素
🔍 动态扩容机制
private void grow(int minCapacity) {int oldCapacity = elementData.length;// 新容量 = 旧容量 + 旧容量/2 (即 1.5 倍)int newCapacity = oldCapacity + (oldCapacity >> 1);// 创建新数组,复制数据elementData = Arrays.copyOf(elementData, newCapacity);
}
内存变化:
扩容前:[Apple][Banana] (容量=2, size=2)
扩容后:[Apple][Banana][ ] (容量=3, size=2)
✅ 建议:初始化时指定合理容量,避免频繁扩容影响性能。
❌ 经典误区:中间插入/删除慢
// 错误:频繁在中间插入/删除,导致后面元素移动,性能 O(n)
list.add(1, "Orange"); // 插入到索引 1
list.remove(1); // 删除索引 1
✅ 结论:
ArrayList
适合查询多、增删少的场景。
2. LinkedList
—— 基于“双向链表”的实现
🎯 核心特性
- 底层是双向链表:每个节点包含前后指针
- 头尾增删快:时间复杂度
O(1)
- 支持栈/队列操作:实现了
Deque
接口 - 非线程安全
LinkedList<String> list = new LinkedList<>();
list.addFirst("Apple"); // 头插
list.addLast("Banana"); // 尾插
String first = list.getFirst(); // 快速获取第一个元素
🖼️ 图示:双向链表的内存布局
地址 2000: +--------+--------+--------+|prev=null| data=A |next=3000|+--------+--------+--------+地址 3000: +--------+--------+--------+|prev=2000| data=B |next=null|+--------+--------+--------+
逻辑结构:
head tail| |v v
+--------+ +--------+
|null|A|<--->|A|B|null|
+--------+ +--------+2000 3000
✅ 适用场景:频繁在头部或尾部增删元素,如消息队列、栈操作。
❌ 经典误区:随机访问慢
// 错误:频繁随机访问,导致遍历整个链表,性能 O(n)
String third = list.get(2); // 需要遍历两次才能找到第三个元素
✅ 结论:
LinkedList
适合头尾操作多、随机访问少的场景。
三、List
的常用方法详解
1. 添加元素
// 在末尾添加
list.add("Apple");// 在指定位置插入
list.add(1, "Banana"); // 插入到索引 1
2. 获取元素
// 通过索引获取
String first = list.get(0);// 获取第一个/最后一个元素
String first = list.get(0);
String last = list.get(list.size() - 1);
3. 删除元素
// 删除指定位置的元素
list.remove(1); // 删除索引 1 的元素// 删除指定对象(第一次出现)
list.remove("Apple");
4. 替换元素
// 替换指定位置的元素
list.set(1, "Grape"); // 将索引 1 的元素替换为 Grape
5. 查找元素
// 判断是否包含某个元素
boolean contains = list.contains("Apple");// 查找元素的位置
int index = list.indexOf("Apple"); // 返回第一个 Apple 的索引
int lastIndex = list.lastIndexOf("Apple"); // 返回最后一个 Apple 的索引
6. 子列表操作
// 获取子列表
List<String> subList = list.subList(1, 3); // 包含索引 1 和 2 的元素
四、List
的线程安全版本
1. Collections.synchronizedList()
—— 简单粗暴的同步
List<String> syncList = Collections.synchronizedList(new ArrayList<>());synchronized (syncList) {syncList.add("Apple");
}
❌ 缺点:
- 整个
List
对象加锁,粒度过大,性能差。- 不支持并发读写,可能导致阻塞。
2. CopyOnWriteArrayList
—— 写时复制的线程安全 List
🎯 核心特性
- 写时复制:每次修改(
add
,remove
)都会创建一个新数组副本。 - 适用于读多写少的场景:读操作无锁,写操作加锁但不影响读。
List<String> cowList = new CopyOnWriteArrayList<>();
cowList.add("Apple");
cowList.add("Banana");
✅ 适用场景:
- 并发读多、写少的场景,如日志记录、事件监听器等。
五、高频问题 & 高分回答
Q1: ArrayList
和 LinkedList
如何选择?
答:
ArrayList
:基于数组,查询快(O(1)
),增删慢(O(n)
);
适合随机访问多、增删少的场景。LinkedList
:基于链表,增删快(O(1)
头尾),查询慢(O(n)
);
适合频繁头尾增删的场景,或实现栈/队列。
我们项目中用户列表用ArrayList
,消息队列用LinkedList
。
Q2: ArrayList
的扩容机制是怎样的?
答:
- 默认容量 10;
- 扩容时,新容量 = 原容量 × 1.5;
- 使用
Arrays.copyOf()
复制数据;- 扩容是耗时操作,建议初始化时指定合理容量。
Q3: LinkedList
的优势和劣势是什么?
答:
- 优势:头尾增删快(
O(1)
),支持栈/队列操作;- 劣势:随机访问慢(
O(n)
),内存开销稍大(每个节点有前后指针)。
适合频繁头尾操作的场景,如消息队列、栈操作。
Q4: CopyOnWriteArrayList
的工作原理?
答:
- 写时复制:每次修改(
add
,remove
)都会创建一个新数组副本;- 读操作无锁,写操作加锁但不影响读;
- 适用于读多写少的场景,如日志记录、事件监听器等。
六、总结:一张表搞懂 List
的选型
场景 | 推荐实现 | 关键点 |
---|---|---|
随机访问多、增删少 | ArrayList | 查询快,扩容注意 |
头尾增删多、随机访问少 | LinkedList | 头尾操作极快,随机访问慢 |
并发读多写少 | CopyOnWriteArrayList | 写时复制,读无锁 |
🔚 最后一句话
List
是 Java 集合框架中最基础、最常用的接口之一。
它不仅仅是一个“容器”,更是我们日常开发中处理有序数据的核心工具。
只有当你真正理解了ArrayList
的扩容机制、LinkedList
的双向链表结构,
以及CopyOnWriteArrayList
的写时复制原理,
你才能写出高效、健壮、专业的 Java 代码!
希望这篇能帮你彻底搞懂 List
接口及其常见实现!