在 Java 开发中,List
是最常用的集合接口之一,它允许存储有序、可重复的元素。JDK 提供了多种 List
实现,每种都有其特定的性能特征和适用场景。
本文将深入剖析 Java 中常见的 List
实现类,解答面试高频问题,帮助你掌握 “何时用哪种 List” 的核心决策能力。
List
是 Collection
的子接口,核心特性:
get(index)
)List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
System.out.println(list.get(0)); // 输出 A
实现类 | 底层结构 | 线程安全 | 随机访问 | 插入/删除 | 适用场景 |
---|---|---|---|---|---|
ArrayList | 动态数组 | ❌ | O(1) | 尾部 O(1),中间 O(n) | 频繁读取、尾部增删 |
LinkedList | 双向链表 | ❌ | O(n) | 任意位置 O(1)(已定位) | 频繁中间插入删除 |
Vector | 动态数组 | ✅(方法同步) | O(1) | 尾部 O(1),中间 O(n) | 老旧系统,不推荐新项目 |
CopyOnWriteArrayList | 写时复制数组 | ✅ | O(1) | O(n)(写操作) | 读多写少的并发场景 |
初始容量:10(无参构造)
扩容策略:原容量的 1.5 倍
int newCapacity = oldCapacity + (oldCapacity >> 1); // 位移优化性能
扩容步骤:
💡 建议:若已知数据量,使用
new ArrayList<>(initialCapacity)
避免频繁扩容。
prev
、next
指针,内存开销大push/pop
)和队列(offer/poll
)list.remove(index)
仍需 O(n) 时间遍历到位置addFirst()
、addLast()
、removeFirst()
等操作❗ 误区澄清: “LinkedList 在任意位置插入都是 O(1)” 是错误说法。 正确说法:定位到节点后,插入是 O(1),但定位本身是 O(n)。
ArrayList
类似,但方法用 synchronized
修饰CopyOnWriteArrayList
或显式同步public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock(); // 写操作加锁
try {
Object[] elements = getArray();
int len = elements.length;
// 复制新数组 +1
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements); // 原子替换引用
return true;
} finally {
lock.unlock();
}
}
get()
不加锁,性能极高ReentrantLock
,保证线程安全✅ 适用场景:读远多于写的并发环境,如:
答案:取决于遍历方式!
遍历方式 | 是否可修改 | 说明 |
---|---|---|
普通 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
的 add()
方法源码:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // ① 检查扩容
elementData[size++] = e; // ② 赋值 + size 自增
return true;
}
在多线程环境下,可能出现以下问题:
elementData[size] = e
,但未执行 size++
elementData[size] = e
(此时 size 未变)size=9
,容量=10,无需扩容size++
后 size=10elementData[10] = e
→ 数组越界!size++
不是原子操作(读取 → +1 → 写回)size=5
,都执行 size=6
,最终只加了1次Collections.synchronizedList()
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
// 注意:遍历时仍需手动同步
synchronized (syncList) {
for (String s : syncList) {
// ...
}
}
CopyOnWriteArrayList
List<String> cowList = new CopyOnWriteArrayList<>();
// 读操作无需同步,写操作自动线程安全
Vector
(不推荐)List<String> vector = new Vector<>();
✅ 推荐顺序:
CopyOnWriteArrayList
>Collections.synchronizedList()
>Vector
场景 | 推荐实现 |
---|---|
频繁按索引访问、遍历 | ✅ ArrayList |
频繁在列表末尾添加/删除 | ✅ ArrayList |
频繁在列表中间插入/删除 | ✅ LinkedList(但需评估是否真需要) |
实现栈或队列 | ✅ LinkedList(或 ArrayDeque) |
并发读多写少 | ✅ CopyOnWriteArrayList |
单线程高性能访问 | ✅ ArrayList |
💡 实际建议:大多数场景优先使用
ArrayList
,除非有明确的中间插入删除需求。
(oldCapacity + oldCapacity >> 1)
优化性能。之所以扩容是 1.5 倍,是因为 1.5 可以充分利用移位操作,减少浮点数或者运算时间和运算次数。
Node
包含 prev
和 next
指针。
volatile
数组引用,保证可见性。
Iterator.remove()
或 CopyOnWriteArrayList
。
Vector
方法同步,扩容翻倍,性能差,已过时。
List
是 Java 集合体系的基石,理解其不同实现的底层结构、性能特征和线程安全机制,是每个 Java 开发者的必备技能。
🔑 核心原则:没有“最好”的 List,只有“最合适”的场景选择。
掌握这些知识,不仅能写出高性能代码,也能在面试中从容应对各种“为什么”和“如何选择”的问题。