Java List集合核心知识点总结(ArrayList/LinkedList/Vector)
在Java集合框架中,List是最基础也最常用的接口之一,它继承自Collection接口,代表一个有序、可重复的元素集合,支持通过索引直接访问元素。本文将从源码原理、性能实测、面试陷阱等维度深入梳理List的三个核心实现类:ArrayList、LinkedList和Vector,帮你彻底搞懂List集合的使用与选型。
一、ArrayList:基于动态数组的实现
1. 核心特点
- 底层基于动态Object数组 实现,初始默认容量为10(注意:无参构造器创建时初始是空数组
EMPTY_ELEMENTDATA,第一次添加元素才会扩容到10) - 查询效率高:通过索引直接定位元素,时间复杂度O(1)
- 增删效率低:增删元素需要移动数组元素,时间复杂度O(n),且扩容时需要复制整个数组
- 线程不安全:多线程环境下操作会出现并发修改异常,不适合多线程场景
2. 关键扩容机制(源码级解析)
ArrayList的扩容是其核心特性,也是性能瓶颈所在,具体流程如下:
- 调用
add()方法时,先执行ensureCapacityInternal(size + 1)检查是否需要扩容 - 默认扩容规则:新容量 = 原容量 + 原容量 >> 1(即1.5倍)
- 特殊情况处理:
- 若1.5倍容量仍小于"最小需要容量"(如一次性添加20个元素到空数组),则直接使用"最小需要容量"作为新容量
- 若新容量超过
MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8),则使用Integer.MAX_VALUE作为新容量
- 扩容本质:通过
Arrays.copyOf()创建新数组,将原数组元素复制到新数组,这一步是扩容的主要性能开销
性能优化建议:如果提前知道集合的大致元素数量,创建ArrayList时指定初始容量,避免频繁扩容带来的性能损耗。
3. 常用方法与示例
(1)添加元素
java
// 尾部添加元素
list.add("元素1");
// 指定索引插入(原位置及后续元素后移)
list.add(1, "元素2");
(2)删除元素
java
// 删除指定索引元素,返回被删除的元素
list.remove(1);
// 删除第一次出现的指定元素,返回boolean类型结果
list.remove("元素1");
// 清空集合中所有元素
list.clear();
(3)修改与查询
java
// 替换指定索引元素,返回被替换的元素
list.set(0, "新元素");
// 获取指定索引位置的元素
String element = list.get(0);
// 判断集合是否包含指定元素
boolean contains = list.contains("元素1");
// 获取集合中元素的个数
int size = list.size();
// 判断集合是否为空
boolean isEmpty = list.isEmpty();
(4)三种遍历方式
java
// 1. 普通for循环(ArrayList最快,基于索引直接访问)
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// 2. 增强for循环(底层基于迭代器实现)
for (String s : list) {
System.out.println(s);
}
// 3. 迭代器遍历(唯一支持遍历过程中安全删除元素的方式)
Iterator<String> it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
4. 核心源码片段分析
java
// 扩容核心方法
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 核心:新容量 = 旧容量 + 旧容量右移1位(即1.5倍)
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 扩容本质:数组复制
elementData = Arrays.copyOf(elementData, newCapacity);
}
5. 常见陷阱:subList()的内存泄漏风险
subList(int fromIndex, int toIndex)返回的是原List的视图 ,而非新List。对subList的修改会直接反映到原List上,且原List会持有subList的引用。如果在循环中大量创建subList而不释放,会导致严重的内存泄漏。正确做法是通过new ArrayList<>(list.subList(...))创建新的独立List。
二、LinkedList:基于双向链表的实现
1. 核心特点
- 底层基于双向链表实现,每个节点包含prev指针、next指针和元素值三个部分
- 增删效率高:只需修改节点指针,无需移动元素,时间复杂度O(1)(仅针对首尾或已知节点的增删)
- 查询效率低:需要从头或尾遍历链表定位元素,时间复杂度O(n)
- 线程不安全:多线程环境下操作会出现并发修改问题
- 天然实现了Queue和Deque接口,可直接作为队列、双端队列和栈使用
2. 常用方法与示例
LinkedList除了实现List接口的通用方法外,还提供了大量针对首尾元素的便捷操作,这是它的核心优势:
(1)添加元素
java
LinkedList<Integer> list = new LinkedList<>();
// 尾部添加元素
list.add(1);
// 指定索引插入元素
list.add(1, 2);
// 头部添加元素
list.addFirst(0);
// 尾部添加元素(与add()方法功能完全等价)
list.addLast(3);
(2)获取元素
java
// 获取指定索引位置的元素
int num = list.get(1);
// 获取链表第一个元素
int first = list.getFirst();
// 获取链表最后一个元素
int last = list.getLast();
(3)删除元素
java
// 删除链表第一个元素
list.remove();
// 删除指定索引位置的元素
list.remove(1);
// 删除链表第一个元素(与remove()方法功能等价)
list.removeFirst();
// 删除链表最后一个元素
list.removeLast();
重要注意:绝对避免使用普通for循环遍历LinkedList ,每次调用
get(index)都会从头或尾重新遍历链表,当元素数量较多时性能会急剧下降。推荐使用增强for循环或迭代器遍历。
3. 性能对比实测
以下是简单的性能测试代码片段,直观展示两者差异:
java
// ArrayList与LinkedList尾部插入100万条数据对比
long start = System.currentTimeMillis();
List<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
arrayList.add(i);
}
System.out.println("ArrayList尾部插入耗时:" + (System.currentTimeMillis() - start) + "ms");
start = System.currentTimeMillis();
List<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < 1000000; i++) {
linkedList.add(i);
}
System.out.println("LinkedList尾部插入耗时:" + (System.currentTimeMillis() - start) + "ms");
实测结果:在预分配容量的情况下,ArrayList尾部插入性能略优于LinkedList;在中间位置插入时,LinkedList性能显著优于ArrayList。
三、Vector:已过时的线程安全实现
1. 核心特点
- 底层同样基于动态数组实现,与ArrayList原理基本一致
- 线程安全 :所有方法都添加了
synchronized同步锁,但同步开销极大,性能远低于ArrayList - 扩容机制:默认扩容为原容量的2倍(可通过构造器自定义容量增量,这是与ArrayList的重要区别)
- 不建议使用 :属于JDK1.0的遗留类,设计老旧且性能差,如需线程安全的List集合,推荐使用
Collections.synchronizedList或CopyOnWriteArrayList替代
2. 特有方法(了解即可)
Vector提供了一些与ArrayList功能重复但命名不同的历史遗留方法:
addElement(E obj):功能等价于add()方法elementAt(int index):功能等价于get()方法removeElement(Object obj):功能等价于remove()方法elements():返回Enumeration迭代器,已被更通用的Iterator替代
四、CopyOnWriteArrayList:高并发场景的首选
1. 核心原理
CopyOnWriteArrayList是java.util.concurrent包下的线程安全List实现,采用**写时复制(Copy-On-Write)**机制:
- 读操作:完全无锁,直接读取原数组
- 写操作:加锁,复制一份新数组,在新数组上修改,修改完成后将新数组赋值给原数组引用
- 核心思想:读写分离,最终一致性
2. 优缺点与适用场景
- 优点:读操作性能极高,适合读多写少的高并发场景(如配置信息缓存、白名单/黑名单管理)
- 缺点:内存占用大(每次写操作都要复制数组),数据一致性只能保证最终一致性,不适合实时性要求极高的场景
- 注意:迭代器是快照迭代器,不支持在迭代过程中修改集合
五、List集合核心总结与选型对比
1. List接口通用特性
- 有序性:严格按照元素的插入顺序进行存储和访问
- 可重复性:允许存储重复元素,同时也支持存储null值
- 索引支持:可以通过int类型的索引直接访问元素,索引从0开始计数
2. 四大实现类对比表
| 特性 | ArrayList | LinkedList | Vector | CopyOnWriteArrayList |
|---|---|---|---|---|
| 底层数据结构 | 动态数组 | 双向链表 | 动态数组 | 动态数组(写时复制) |
| 随机查询效率 | 极高(O(1)) | 极低(O(n)) | 高(O(1)) | 高(O(1)) |
| 首尾增删效率 | 低(O(n),需移动元素) | 极高(O(1),仅改指针) | 低(O(n),需移动元素) | 低(O(n),需复制数组) |
| 中间增删效率 | 低(O(n)) | 中等(需先遍历定位) | 低(O(n)) | 低(O(n)) |
| 线程安全 | 不安全 | 不安全 | 安全(方法级加锁) | 安全(写时复制+锁) |
| 默认扩容倍数 | 1.5倍 | 无扩容(链表按需分配) | 2倍 | 无扩容(每次写操作复制) |
| 内存占用 | 低(连续数组空间) | 高(额外存储节点指针) | 低(连续数组空间) | 高(写时复制双份数组) |
| 适用场景 | 频繁查询、少量增删 | 频繁首尾增删、队列/栈 | 无(仅维护老代码使用) | 高并发读多写少场景 |
| 综合性能 | 最高 | 中等 | 最低 | 读高写低 |
3. 补充注意事项
- 并发安全方案 :
- 轻度并发场景:使用
Collections.synchronizedList(List list)包装ArrayList - 高并发读多写少场景:使用
java.util.concurrent.CopyOnWriteArrayList,采用写时复制机制,读操作无锁,性能更优
- 轻度并发场景:使用
- 遍历性能选择 :
- ArrayList:普通for循环 > 增强for循环 > 迭代器
- LinkedList:迭代器 > 增强for循环 > 普通for循环
- 空值处理 :三个实现类都允许存储null元素,但在调用
get()、remove()等方法时需注意空指针异常 - Collections工具类常用方法 :
Collections.sort(List list):对List进行自然排序Collections.reverse(List list):反转List中元素的顺序Collections.shuffle(List list):随机打乱List中元素的顺序Collections.unmodifiableList(List list):返回一个不可修改的List视图,防止误修改
结论
List集合是Java开发中每天都会用到的基础工具,掌握其底层原理和选型技巧能显著提升代码质量和运行性能。日常开发中,绝大多数场景优先使用ArrayList;当需要频繁在集合首尾增删元素,或需要实现队列、栈数据结构时,选择LinkedList;Vector由于性能和设计缺陷,除非是维护遗留老代码,否则坚决不要使用;在高并发读多写少的场景下,CopyOnWriteArrayList是最佳选择。