List 是什么
List 是 Collection 家族里最常用的一种:有序、可重复、能通过下标访问。初学者最先接触的通常是 ArrayList,面试里又总被问 ArrayList 和 LinkedList 的区别。与其背结论,不如先弄清楚它们各自把数据存在哪里。
实际项目里 90% 的场景用 ArrayList 就够了,LinkedList 出现的频率比想象中低。真正需要队列或栈时,ArrayDeque 往往比 LinkedList 更合适。
ArrayList:数组的包装
ArrayList 内部就是一个 Object[]。第一次 add 时才会分配默认长度 10 的数组,装不下时按 1.5 倍扩容,把老数组复制到新数组。所以:
- 按下标
get/set是 O(1),非常快; - 中间插入、删除要移动后面所有元素,是 O(n);
- 尾部
add均摊 O(1),但扩容那一次会明显变慢。
import java.util.ArrayList;
import java.util.List;
public class ArrayListDemo {
public static void main(String[] args) {
List<String> names = new ArrayList<>(16); // 预估容量,少扩容
names.add("Amy");
names.add("Bob");
names.add(0, "Cindy"); // 头部插入,后面元素整体后移
System.out.println(names); // [Cindy, Amy, Bob]
System.out.println(names.get(1)); // Amy
names.remove("Bob");
System.out.println(names.size()); // 2
}
}
remove 有两个重载:remove(int index) 按下标删,remove(Object o) 按内容删。List<Integer> 里写 list.remove(1) 删的是下标 1,不是值为 1 的元素,这个坑非常经典。
扩容时要申请新数组并复制全部元素,这也是为什么能预估大小时一定要传初始容量。ArrayList 不是线程安全的,多线程同时 add 可能丢元素,甚至抛数组越界异常;并发读多的场景用 CopyOnWriteArrayList,写多的场景在外层加锁或者换用队列。
LinkedList:双向链表
LinkedList 的每个节点都保存前驱和后继指针。头尾增删是 O(1),按下标访问要顺着链表走,是 O(n)。它比 ArrayList 多存两个引用,内存占用更大,缓存局部性也差,所以“ LinkedList 插入删除更快”这句话只在“已经定位到节点”的前提下成立。
真要用链表结构,先想清楚是不是队列或栈:是的话用 ArrayDeque,它基于循环数组,性能通常更好,还禁止存 null。
LinkedList 每个节点额外存两个指针,内存开销大约是 ArrayList 的两三倍,遍历时缓存命中率也低。除非确实要在头部频繁插入,否则没必要为了“链表插入快”这个直觉选它;真要在中间频繁插入,先确认自己是否真能拿到那个位置的节点。另外,Vector 和 Stack 属于老 API,方法上加了 synchronized,性能一般,新代码不要再用。
怎么选
| 场景 | 推荐 | 原因 |
|---|---|---|
| 按位置读取、遍历为主 | ArrayList |
数组随机访问快,内存连续 |
| 只在尾部追加 | ArrayList |
均摊 O(1) |
| 频繁在头部插入 | LinkedList |
头插 O(1) |
| 当队列/栈用 | ArrayDeque |
比 LinkedList 更轻量 |
| 读多写少的并发场景 | CopyOnWriteArrayList |
读不加锁,写时复制 |
常见坑
1. Arrays.asList 返回的是定长列表
List<String> list = Arrays.asList("a", "b");
list.add("c"); // UnsupportedOperationException
它返回的是 Arrays 内部的私有实现,底层还是原数组,只能改不能增删。需要可变列表就再包一层:new ArrayList<>(Arrays.asList("a", "b"))。
2. subList 是视图不是副本
list.subList(1, 3) 拿到的是原列表的一段“窗口”。之后只要原列表被结构性修改(增删元素),再访问这个窗口就会抛 ConcurrentModificationException。想长期保存,复制出来:new ArrayList<>(list.subList(1, 3))。
3. 遍历时删除元素
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if ("b".equals(it.next())) {
it.remove();
}
}
增强 for 里直接 list.remove() 会抛 ConcurrentModificationException。更简洁的写法是 list.removeIf("b"::equals),JDK 8 之后都支持。
4. 忘记指定初始容量
默认容量是 10,存 1000 条数据要扩容七八次,每次都要复制数组。能预估大小时写成 new ArrayList<>(expectedSize),性能会稳定很多。
5. 用 contains 做去重
List.contains 是 O(n),外层再套一层循环就是 O(n²)。数据量上千就该换 HashSet,需要保留顺序用 LinkedHashSet。
排查步骤
- 看到
UnsupportedOperationException,先确认 List 是不是Arrays.asList、List.of或Collections.unmodifiableList产生的; - 看到
ConcurrentModificationException,检查遍历过程中有没有增删元素; - 接口返回 List 时不要返回 null,没有数据就返回空列表,调用方可以少一次判空;
- 性能问题先看有没有在循环里调用
contains、indexOf; - 数据量超过一万时,优先考虑换数据结构,而不是继续调
ArrayList的参数。
评论