Java中的各种list有什么区别及list和set区别详析
Java中List常见实现:ArrayList(基于动态数组,查询速度快)、LinkedList(基于双向链表,头尾操作效率高)、CopyOnWriteArrayList(写时复制,适合高并发读多写少),Vector与Stack已淘汰。List有序、可重复、有下标;Set无序、不可重复、无下标,依赖于hashCode与equals去重。
Ja va 中 List 的几种实现
List 是 Ja va 集合框架中一个非常核心的接口,它代表着有序、可重复的元素集合。说到有序,就好比排队,谁先来谁就排在前面,元素的位置由插入顺序决定。而可重复,则意味着你可以把同一个对象往里放多次。

在实践中,我们最常遇到的 List 实现有这么几个:ArrayList、LinkedList、Vector、Stack,以及并发场景下的 CopyOnWriteArrayList。每个都有自己独特的脾气和适用场景,搞清楚它们之间的区别,是写出高性能代码的基础。
ArrayList
底层结构
ArrayList 的底层,是一个动态数组。它初始化时的默认容量是 10,但这不是固定的。当元素数量超过当前容量时,它会触发扩容机制。扩容规则很简单:
新容量 = 旧容量 + 旧容量 / 2,也就是 1.5 倍。例如容量 10 的数组,扩一次变成 15,再扩一次变成 22,以此类推。
时间复杂度
| 操作 | 复杂度 | 说明 |
|---|---|---|
| 随机访问 get(i) | O(1) | 直接通过数组下标访问 |
| 尾部 add | O(1) 均摊 | 扩容时退化为 O(n) |
| 中间插入/删除 | O(n) | 需要移动后续所有元素 |
线程安全
ArrayList 是非线程安全的。如果多个线程同时对它进行结构修改(比如一个线程在遍历,另一个在添加/删除),很容易出问题。解决方案通常用 Collections.synchronizedList 进行包装,或者直接上并发包里的 CopyOnWriteArrayList。
LinkedList
底层结构
LinkedList 的底层是双向链表。每个节点由三部分组成:前驱指针(prev)、存储的数据(item)、后继指针(next)。这意味着它的内存是分散的,不连续。
时间复杂度
| 操作 | 复杂度 | 说明 |
|---|---|---|
| get(i) | O(n) | 需要从头或尾遍历到指定位置 |
| 头/尾插入删除 | O(1) | 只需要修改指针指向 |
| 中间插入 | O(n) | 先要找到那个位置 |
Vector
底层结构
Vector 的底层也是动态数组,这一点和 ArrayList 几乎一样。但它有一个关键区别:线程安全。
关键区别
Vector 的所有方法都加了 synchronized,这意味着它在多线程环境下是安全的。
问题
不过,这种锁粒度太粗了,性能非常差。实际项目中,几乎没有理由再去使用 Vector,它已经被 JCF 体系淘汰了。能用 ArrayList 或者并发集合解决的,坚决不用 Vector。
Stack
继承关系
Stack extends Vector
特点
Stack 是典型的后进先出(LIFO)结构。因为它继承了 Vector,所以也是线程安全的。但同样的,由于 Vector 本身的问题,Stack 也属于“已淘汰”的组件。更推荐用 Deque 接口的实现类(比如 ArrayDeque)来模拟栈的行为。
CopyOnWriteArrayList(并发重点)
底层思想
CopyOnWriteArrayList 是并发包下的一个利器,它的核心思想是“写时复制”。
特点
| 方面 | 说明 |
|---|---|
| 线程安全 | ✅ |
| 读性能 | 非常高 |
| 写性能 | 较差(每次写都要复制整个底层数组) |
| 迭代 | 不会抛 ConcurrentModificationException |
它的读操作完全不加锁,读的是快照数组,所以读性能极好。但写的代价也很高——每次修改(add、set、remove)都会先复制一份新数组,修改完后再将引用指向新数组。因此它非常适合“读多写少”的并发场景,比如缓存配置、黑白名单等。
对比总结
| 实现 | 底层 | 线程安全 | 适合场景 |
|---|---|---|---|
| ArrayList | 动态数组 | ❌ | 随机查询多、数据变化不频繁 |
| LinkedList | 双向链表 | ❌ | 频繁的头尾插入删除操作 |
| Vector | 动态数组 | ✅ | 已淘汰,不建议使用 |
| Stack | 栈 | ✅ | 已淘汰,用 Deque 代替 |
| CopyOnWriteArrayList | 数组复制 | ✅ | 高并发读多写少场景 |
面试常见问题
Q1:ArrayList 和 LinkedList 区别?
ArrayList 底层是数组,查询快(O(1)),插入慢(O(n));
LinkedList 底层是链表,头尾操作快(O(1)),随机访问慢(O(n))。
Q2:为什么 ArrayList 不是线程安全?
在并发环境下,多个线程同时调用 add / remove 可能导致:
扩容时数据丢失、元素覆盖、操作被中断导致数据不一致。
Q3:CopyOnWriteArrayList 为什么读快?
读操作没有加锁,始终读取的是当前底层数组的一个稳定快照,所以不会出现并发修改异常(ConcurrentModificationException)。
Q4:为什么不推荐 Vector / Stack?
因为它们的 synchronized 锁粒度太大,性能差,且其他并发容器(如 CopyOnWriteArrayList、ConcurrentLinkedDeque)提供了更好的替代方案。
总结
ArrayList 查得快,LinkedList 头尾插得快,
并发专用 CopyOnWrite,Vector 和 Stack 让它歇。
一、关于List和Set
List vs Set
| 对比点 | List | Set |
|---|---|---|
| 是否有序 | ✅ 有序(按插入顺序) | ❌ 大多无序(LinkedHashSet 例外) |
| 是否允许重复 | ✅ 允许 | ❌ 不允许 |
| 是否有下标 | ✅ 有(get(i)) | ❌ 没有 |
| 常见实现 | ArrayList、LinkedList | HashSet、LinkedHashSet、TreeSet |
| 典型用途 | 保存“有顺序、可重复”的数据 | 保存“去重”的数据 |
一句话记忆:
List = 有顺序 + 可重复
Set = 去重
二、List
什么是List?
- 可以理解为数组的升级版。
- 能按顺序存放元素。
- 能存储重复的元素。
- 能通过下标(索引)直接访问。
Listlist = new ArrayList<>(); list.add(10); list.add(10); list.add(20); System.out.println(list); // [10, 10, 20] System.out.println(list.get(1)); // 10
三、Set
什么是Set?
- 天生自带去重光环。
- 它关心插入顺序?不一定,要看具体实现(HashSet 无序,LinkedHashSet 保持顺序,TreeSet 按自然顺序排序)。
- 没有下标,不能用 get(i) 来访问元素。
Setset = new HashSet<>(); set.add(10); set.add(10); set.add(20); System.out.println(set); // [10, 20]
Set 是怎么判断“重复”的?
依赖两个方法:hashCode() + equals()。
- 先计算元素的 hashCode 值,定位到哈希桶。
- 再用 equals 方法比较该桶内是否已有相同元素。
四、List vs Set 核心区别
List 和 Set 都是 Collection 的子接口,核心区别在于:是否允许重复、是否有顺序、是否有下标。List 适合按顺序存储可重复数据,Set 天生去重、无下标,完全依赖哈希值判断唯一性。
五、一句话总结
List:顺序 + 重复 + 下标
Set:去重 + 无下标 + 基于 equals / hashCode

































