【集合框架List进阶】
在 Java 开发中,List
是最常用的集合接口之一,它允许存储有序、可重复的元素。JDK 提供了多种 List
实现,每种都有其特定的性能特征和适用场景。
本文将深入剖析 Java 中常见的 List
实现类,解答面试高频问题,帮助你掌握 “何时用哪种 List” 的核心决策能力。
一、List 接口概览
List
是 Collection
的子接口,核心特性:
- ✅ 元素有序(按插入顺序)
- ✅ 允许重复元素
- ✅ 支持通过索引访问(
get(index)
) - ✅ 提供插入、删除、查找等操作
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
System.out.println(list.get(0)); // 输出 A
二、常见 List 实现类对比
实现类 | 底层结构 | 线程安全 | 随机访问 | 插入/删除 | 适用场景 |
---|---|---|---|---|---|
ArrayList | 动态数组 | ❌ | O(1) | 尾部 O(1),中间 O(n) | 频繁读取、尾部增删 |
LinkedList | 双向链表 | ❌ | O(n) | 任意位置 O(1)(已定位) | 频繁中间插入删除 |
Vector | 动态数组 | ✅(方法同步) | O(1) | 尾部 O(1),中间 O(n) | 老旧系统,不推荐新项目 |
CopyOnWriteArrayList | 写时复制数组 | ✅ | O(1) | O(n)(写操作) | 读多写少的并发场景 |
三、核心实现类详解
1. ArrayList:基于动态数组
特点:
- 随机访问快:通过索引直接定位,时间复杂度 O(1)
- 尾部操作高效:在末尾添加/删除元素,时间复杂度 O(1)
- 中间操作慢:插入或删除需移动后续元素,O(n)
- 内存连续:占用连续内存空间,缓存友好
扩容机制:
- 初始容量:10(无参构造)
- 扩容策略:原容量的 1.5 倍
int newCapacity = oldCapacity + (oldCapacity >> 1); // 位移优化性能
- 扩容步骤:
- 检查是否需要扩容
- 创建新数组(1.5倍)
- 复制原数组元素
- 更新引用
💡 建议:若已知数据量,使用
new ArrayList<>(initialCapacity)
避免频繁扩容。
2. LinkedList:基于双向链表
特点:
- 插入/删除快:只需修改前后节点指针,O(1)(前提是已定位到位置)
- 随机访问慢:需从头或尾遍历,O(n)
- 内存非连续:每个节点包含
prev
、next
指针,内存开销大 - 支持双端操作:天然适合实现栈(
push/pop
)和队列(offer/poll
)
注意:
list.remove(index)
仍需 O(n) 时间遍历到位置- 真正高效的是
addFirst()
、addLast()
、removeFirst()
等操作
❗ 误区澄清:
“LinkedList 在任意位置插入都是 O(1)” 是错误说法。
正确说法:定位到节点后,插入是 O(1),但定位本身是 O(n)。
3. Vector:线程安全的 ArrayList
特点:
- 与
ArrayList
类似,但方法用synchronized
修饰 - 扩容策略:翻倍扩容(原容量 × 2)
- 性能较差:同步带来额外开销
- 不推荐使用:现代并发编程应使用
CopyOnWriteArrayList
或显式同步
4. CopyOnWriteArrayList:读写分离的并发 List
核心机制:写时复制(Copy-On-Write)
public boolean add(E e) {final ReentrantLock lock = this.lock;lock.lock(); // 写操作加锁try {Object[] elements = getArray();int len = elements.length;// 复制新数组 +1Object[] newElements = Arrays.copyOf(elements, len + 1);newElements[len] = e;setArray(newElements); // 原子替换引用return true;} finally {lock.unlock();}
}
特点:
- ✅ 读操作无锁:
get()
不加锁,性能极高 - ✅ 写操作安全:加
ReentrantLock
,保证线程安全 - ✅ 最终一致性:读可能看到旧数据(适合事件监听、配置管理)
- ❌ 写性能低:每次写都复制数组,O(n)
- ❌ 内存占用高:存在多个数组副本
✅ 适用场景:读远多于写的并发环境,如:
- 监听器列表
- 缓存配置
- 白名单/黑名单
四、List 遍历过程中能否修改?(高频面试题)
答案:取决于遍历方式!
遍历方式 | 是否可修改 | 说明 |
---|---|---|
普通 for 循环 | ✅ | 直接通过 set(index, value) 修改 |
增强 for 循环(foreach) | ❌ | 底层使用 Iterator ,会抛 ConcurrentModificationException |
Iterator 迭代器 | ✅(有限制) | 只能用 iterator.remove() 或 iterator.set() |
CopyOnWriteArrayList + foreach | ✅ | 写时复制机制允许并发修改 |
示例代码:
// ✅ 正确:普通 for 循环修改
for (int i = 0; i < list.size(); i++) {list.set(i, list.get(i) * 2);
}// ❌ 错误:foreach 中直接修改
for (Integer num : list) {list.remove(num); // 抛 ConcurrentModificationException
}// ✅ 正确:使用 Iterator
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {Integer num = it.next();if (num == 2) {it.remove(); // 使用迭代器的 remove// it.set(4); // 修改值}
}
💡 原理:
ArrayList
等集合使用modCount
记录结构修改次数,迭代器会检查expectedModCount
,不一致则抛异常。
五、如何快速删除指定下标的元素?
实现类 | 删除方法 | 时间复杂度 | 说明 |
---|---|---|---|
ArrayList | remove(int index) | O(n) | 删除后需移动后续元素 |
LinkedList | remove(int index) | O(n) | 需先遍历到位置,再修改指针 |
CopyOnWriteArrayList | remove(int index) | O(n) | 复制新数组,写时复制 |
⚠️ 注意:即使是
LinkedList
,remove(index)
也不是 O(1),因为需要遍历定位。
优化建议:
- 若频繁删除首尾元素:使用
LinkedList
的removeFirst()
/removeLast()
- 若已知节点引用:
LinkedList
可通过remove(Node)
实现 O(1)
六、ArrayList 为什么不是线程安全的?(深入源码)
ArrayList
的 add()
方法源码:
public boolean add(E e) {ensureCapacityInternal(size + 1); // ① 检查扩容elementData[size++] = e; // ② 赋值 + size 自增return true;
}
在多线程环境下,可能出现以下问题:
1. 元素为 null
- 线程 A 执行到
elementData[size] = e
,但未执行size++
- 线程 B 也执行
elementData[size] = e
(此时 size 未变) - 两个线程都写入同一个位置,后写的覆盖前写的,导致某个元素丢失,后续位置为 null
2. 索引越界
- 线程 A 判断
size=9
,容量=10,无需扩容 - 线程 B 同样判断无需扩容
- 线程 A 执行
size++
后 size=10 - 线程 B 执行
elementData[10] = e
→ 数组越界!
3. size 不一致
size++
不是原子操作(读取 → +1 → 写回)- 两个线程同时读取
size=5
,都执行size=6
,最终只加了1次
七、如何让 ArrayList 线程安全?
方法 1:使用 Collections.synchronizedList()
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
// 注意:遍历时仍需手动同步
synchronized (syncList) {for (String s : syncList) {// ...}
}
方法 2:使用 CopyOnWriteArrayList
List<String> cowList = new CopyOnWriteArrayList<>();
// 读操作无需同步,写操作自动线程安全
方法 3:使用 Vector
(不推荐)
List<String> vector = new Vector<>();
✅ 推荐顺序:
CopyOnWriteArrayList
>Collections.synchronizedList()
>Vector
八、ArrayList vs LinkedList:如何选择?
场景 | 推荐实现 |
---|---|
频繁按索引访问、遍历 | ✅ ArrayList |
频繁在列表末尾添加/删除 | ✅ ArrayList |
频繁在列表中间插入/删除 | ✅ LinkedList (但需评估是否真需要) |
实现栈或队列 | ✅ LinkedList (或 ArrayDeque ) |
并发读多写少 | ✅ CopyOnWriteArrayList |
单线程高性能访问 | ✅ ArrayList |
💡 实际建议:大多数场景优先使用
ArrayList
,除非有明确的中间插入删除需求。
九、常见问题
ArrayList 扩容机制?
→ 1.5 倍扩容,使用(oldCapacity + oldCapacity >> 1)
优化性能。之所以扩容是 1.5 倍,是因为 1.5 可以充分利用移位操作,减少浮点数或者运算时间和运算次数。LinkedList 是双向链表吗?
→ 是,Node
包含prev
和next
指针。CopyOnWriteArrayList 为什么读不加锁?
→ 写时复制 +volatile
数组引用,保证可见性。遍历 List 时删除元素如何避免异常?
→ 使用Iterator.remove()
或CopyOnWriteArrayList
。ArrayList 和 Vector 的区别?
→Vector
方法同步,扩容翻倍,性能差,已过时。
结语
List
是 Java 集合体系的基石,理解其不同实现的底层结构、性能特征和线程安全机制,是每个 Java 开发者的必备技能。
🔑 核心原则:没有“最好”的 List,只有“最合适”的场景选择。
掌握这些知识,不仅能写出高性能代码,也能在面试中从容应对各种“为什么”和“如何选择”的问题。