作为 Java 开发者,集合框架是日常开发中最高频使用的基础工具,也是 Java 面试中 100% 覆盖的核心考点。很多开发者能够熟练调用集合 API,却对底层数据结构、实现逻辑一知半解,导致选型出错、踩性能坑,面试时也只能答出表面特性,讲不清设计逻辑。
本文系统梳理 Java 中所有主流常用集合,从底层数据结构、核心实现原理、横向性能对比、高频面试考点四个维度全面拆解,覆盖普通集合与并发集合,包含 List、Set、Map、队列 / 双端队列四大分支,补足优先级队列、跳表集合、写时复制集合、阻塞队列等易被忽略的核心内容,形成完整的集合知识体系。

一、Java 集合框架全景与核心数据结构总览
Java 集合本质是「经典数据结构的工程化实现」,每一种集合对应一种或多种数据结构,在时间、空间、功能、线程安全之间做不同的工程权衡。
1. 集合家族完整图谱
1. 集合家族完整图谱
java.util.Collection(单元素集合)
├── List 有序、可重复、支持索引访问
│ ├── ArrayList 动态数组
│ ├── LinkedList 双向链表
│ ├── Vector 同步动态数组(遗留类)
│ └── CopyOnWriteArrayList 写时复制动态数组(并发)
├── Set 无序、元素唯一
│ ├── HashSet 哈希表(基于HashMap)
│ ├── LinkedHashSet 哈希表+双向链表(基于LinkedHashMap)
│ ├── TreeSet 红黑树(基于TreeMap)
│ ├── CopyOnWriteArraySet 写时复制数组(并发)
│ └── ConcurrentSkipListSet 跳表(并发有序)
└── Queue / Deque 队列/双端队列
├── ArrayDeque 循环数组(双端队列)
├── PriorityQueue 二叉堆(优先级队列)
├── ArrayBlockingQueue 数组+单锁(有界阻塞队列)
├── LinkedBlockingQueue 链表+双锁(阻塞队列)
├── SynchronousQueue 无缓冲同步队列
└── ConcurrentLinkedQueue 链表+CAS(无锁非阻塞队列)
java.util.Map(键值对集合)
├── HashMap 数组+链表+红黑树
├── LinkedHashMap HashMap + 双向链表
├── TreeMap 红黑树(有序)
├── Hashtable 哈希表+全表锁(遗留类)
├── ConcurrentHashMap CAS + synchronized 细粒度锁
└── ConcurrentSkipListMap 跳表(并发有序)
2. 核心数据结构对照表
所有集合的性能差异,本质上都是底层数据结构的差异。
| 数据结构 | 核心特点 | 代表集合 | 平均时间复杂度 | 空间特性 |
|---|---|---|---|---|
| 动态数组 | 连续内存、支持随机访问 | ArrayList、Vector | 查 O (1)、尾增 O (1)、中间增删 O (n) | 有预留空间,扩容有性能损耗 |
| 双向链表 | 节点离散、首尾操作快 | LinkedList | 首尾增删 O (1)、查询 O (n) | 每个节点额外存储前后指针 |
| 循环数组 | 头尾指针、双端操作快 | ArrayDeque | 首尾增删 O (1)、查询 O (n) | 容量始终为 2 的幂 |
| 哈希表 | 哈希映射、快速查找 | HashMap、HashSet | 平均 O (1),极端 O (n) | 加载因子 0.75,空间换时间 |
| 红黑树 | 自平衡二叉搜索树 | TreeMap、TreeSet | 增删查均为 O (logn) | 节点存储颜色、父子节点指针 |
| 二叉堆 | 完全二叉树、数组实现 | PriorityQueue | 增 O (logn)、取最值 O (1) | 数组存储,空间利用率高 |
| 跳表 | 多层索引、有序查找 | ConcurrentSkipListMap | 平均 O (logn) | 多层索引,空间开销略大 |
| 写时复制 | 修改时复制副本,读写分离 | CopyOnWriteArrayList | 读 O (1)、写 O (n) | 写操作复制全量数组,内存开销 |
二、List 集合:有序、可重复
List 的核心特征是元素按插入顺序存储、允许重复、支持按索引随机访问,是日常开发中使用最频繁的集合分支。
1. ArrayList
底层数据结构
Object 类型的动态数组,核心字段:
Object[] elementData:存储元素的数组缓冲区size:集合中实际存储的元素个数modCount:结构修改次数,用于 fail-fast 并发修改校验
核心实现原理
- 初始化策略 :JDK1.8 开始采用懒加载,无参构造方法仅创建一个空数组,第一次执行
add()时才真正初始化为容量 10 的数组 - 扩容机制 :元素数量超过数组容量时触发扩容,新容量 = 旧容量 + 旧容量 >> 1(即扩容为原来的 1.5 倍),通过
Arrays.copyOf()批量复制元素到新数组 - 随机访问:通过数组下标直接定位元素,时间复杂度 O (1)
- 中间增删 :需要通过
System.arraycopy()批量移动后续元素,时间复杂度 O (n)
优缺点与适用场景
- 优势:随机访问极快,内存连续 CPU 缓存友好,内存开销小
- 劣势:中间增删性能差,扩容有一次性性能损耗
- 适用场景:绝大多数业务场景的首选,适合读多写少、增删主要在尾部的场景
2. LinkedList
底层数据结构
双向链表 ,每个元素是内部类Node<E>节点:
java
private static class Node<E> {
E item;
Node<E> next; // 后继指针
Node<E> prev; // 前驱指针
}
集合同时持有first头指针和last尾指针。
核心实现原理
- 首尾增删:仅需修改相邻节点的指针,时间复杂度 O (1)
- 指定位置操作:会根据索引位置选择从头或从尾遍历定位节点,时间复杂度 O (n)
- 随机访问:必须遍历链表,性能极差
- 无扩容问题:节点按需创建销毁,不需要预留空间,理论上容量无上限
常见误区
很多人认为 LinkedList 中间插入性能优于 ArrayList,实际上并非如此:中间插入前需要先遍历定位节点,这一步已经是 O (n) 的开销,仅在「已经持有目标节点引用」的场景下,插入性能才优于 ArrayList。
优缺点与适用场景
- 优势:首尾增删无扩容损耗,理论容量无上限
- 劣势:随机访问极慢,节点指针内存开销大,CPU 缓存不友好
- 适用场景:仅在频繁头尾增删、完全不需要随机访问的场景使用;实现栈 / 队列时优先选择 ArrayDeque 而非 LinkedList
3. Vector
底层数据结构
与 ArrayList 完全一致,底层也是动态 Object 数组。
核心实现原理
- 几乎所有 public 方法都加了
synchronized修饰,通过锁整个集合对象实现线程安全 - 默认扩容为原来的 2 倍,可通过构造参数指定容量增长步长
为什么不推荐使用
- 锁粒度太粗,所有方法串行执行,并发性能极低
- 属于 JDK1.0 的遗留类,设计老旧,官方早已不推荐
- 即使加锁,复合操作(如先检查后添加)也不是原子的,仍然存在并发问题
4. CopyOnWriteArrayList(并发)
底层数据结构
写时复制的动态数组 ,底层数组用volatile修饰,保证数组引用的内存可见性。
核心实现原理:写时复制(COW)
- 读操作:直接读取原数组,完全无锁,性能极高
- 写操作:加独占锁,复制一份全新的数组,在新数组上执行修改,修改完成后将数组引用指向新数组,最后释放锁
- 迭代器基于创建时的数组快照遍历,属于 fail-safe 机制,不会抛出并发修改异常
优缺点与适用场景
- 优势:读操作完全无锁,并发读性能极高,线程安全
- 劣势:写操作开销大,每次修改都要复制整个数组;内存占用高;数据是弱一致性的,读不到刚写入的数据
- 适用场景:读多写少的并发场景,如配置列表、黑白名单等更新不频繁的数据
List 四大实现完整对比
| 特性 | ArrayList | LinkedList | Vector | CopyOnWriteArrayList |
|---|---|---|---|---|
| 底层结构 | 动态数组 | 双向链表 | 动态数组 | 写时复制动态数组 |
| 随机访问 | O (1),极快 | O (n),极慢 | O (1),极快 | O (1),极快 |
| 尾部增删 | O(1) | O(1) | O(1) | O (n),需复制数组 |
| 中间增删 | O (n),移动元素 | O (n),遍历定位 | O(n) | O(n) |
| 线程安全 | 不安全,fail-fast | 不安全,fail-fast | 安全,全表锁 | 安全,写时复制 |
| 扩容机制 | 1.5 倍扩容 | 无扩容 | 2 倍扩容 | 每次写都扩容 |
| 内存占用 | 小,有预留空间 | 大,指针开销高 | 小 | 大,写时双份数组 |
| 数据一致性 | 强一致 | 强一致 | 强一致 | 弱一致 |
| 推荐指数 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐ | ⭐⭐⭐ |
三、Set 集合:无序、元素唯一
Set 的核心特征是元素不重复,所有 Set 实现本质上都是对应 Map 的「马甲」------ 仅使用 Map 的 key 存储元素,value 是一个固定的静态常量对象。
1. HashSet
底层数据结构
完全基于HashMap 实现,所有元素作为 HashMap 的 key 存入,value 是固定的静态对象PRESENT = new Object()。
去重原理
完全依赖 HashMap 的 key 去重逻辑:
- 计算元素的 hashCode 定位数组下标
- 下标位置已有元素则遍历链表 / 红黑树,通过
equals()比对 - key 相同则覆盖 value(无实际意义),key 不同则新增节点
自定义对象作为 HashSet 元素时,必须同时重写
hashCode()和equals(),否则会出现元素重复、查询不到的问题。
特性与适用场景
- 无序:不保证遍历顺序,也不保证顺序随时间不变
- 允许存入 1 个 null 元素
- 增删查平均 O (1),是性能最好的 Set 实现
- 通用去重场景的首选
2. LinkedHashSet
底层数据结构
继承自 HashSet,底层基于LinkedHashMap,在哈希表基础上额外维护一条贯穿所有节点的双向链表,用于记录元素的插入顺序。
特性与适用场景
- 插入有序:遍历顺序与元素插入顺序一致
- 性能略低于 HashSet,因为需要额外维护双向链表
- 允许 1 个 null 元素
- 适用于需要去重同时保留插入顺序的场景
3. TreeSet
底层数据结构
基于**TreeMap(红黑树)**实现,元素作为 TreeMap 的 key 存储。
去重与排序原理
- 不依赖 hashCode/equals,而是通过
Comparable.compareTo()或Comparator.compare()返回 0 判断元素重复 - 元素按自然排序或自定义比较器排序,而非插入顺序
- 不允许 null 元素(无法比较大小)
特性与适用场景
- 排序有序,支持范围查询(获取大于 / 小于某个值的元素)
- 增删查均为 O (logn),性能低于 HashSet
- 适用于需要去重同时对元素排序、需要范围查询的场景
4. CopyOnWriteArraySet(并发)
底层数据结构
基于CopyOnWriteArrayList实现,本质是封装了一层去重逻辑的写时复制数组。
特性与适用场景
- 线程安全,读多写少场景适用
- 插入时需要遍历数组检查重复,性能较差
- 弱一致性,迭代器基于快照
- 适用于并发环境下、数据量小、更新不频繁的去重场景
5. ConcurrentSkipListSet(并发)
底层数据结构
基于**ConcurrentSkipListMap(跳表)**实现,是并发版本的有序 Set。
特性与适用场景
- 线程安全,基于跳表的无锁并发设计
- 排序有序,支持并发场景下的范围查询
- 增删查平均 O (logn)
- 适用于高并发环境下需要有序去重的场景
Set 五大实现完整对比
| 特性 | HashSet | LinkedHashSet | TreeSet | CopyOnWriteArraySet | ConcurrentSkipListSet |
|---|---|---|---|---|---|
| 底层结构 | HashMap | LinkedHashMap | TreeMap(红黑树) | CopyOnWriteArrayList | ConcurrentSkipListMap |
| 顺序性 | 无序 | 插入有序 | 排序有序 | 插入有序 | 排序有序 |
| 去重依据 | hashCode+equals | hashCode+equals | compareTo/compare | equals | compareTo/compare |
| null 支持 | 1 个 null | 1 个 null | 不支持 | 支持 | 不支持 |
| 线程安全 | 不安全 | 不安全 | 不安全 | 安全 | 安全 |
| 平均时间复杂度 | O(1) | O(1) | O(logn) | 写 O (n),读 O (1) | O(logn) |
| 性能 | 最好 | 略低 | 中等 | 写极慢,读极快 | 较好 |
| 适用场景 | 通用去重 | 去重 + 保插入顺序 | 去重 + 排序 | 并发读多写少 | 并发有序去重 |
四、Map 集合:键值对存储
1. HashMap
底层数据结构
JDK1.8 后为数组 + 链表 + 红黑树,JDK1.7 为数组 + 单向链表。
核心特性总结
- 初始容量 16,加载因子 0.75,扩容为原来的 2 倍
- 数组长度始终为 2 的幂,通过位运算替代取模计算下标
- 链表长度≥8 且数组容量≥64 时转红黑树,树节点≤6 时退回链表
- 线程不安全,并发扩容会出现数据覆盖等问题
- 通用键值对存储的首选,详细原理可参考 HashMap 专题文章
2. LinkedHashMap
底层数据结构
继承自 HashMap,在其基础上给每个节点增加了before和after指针,形成一条贯穿所有节点的双向链表,用于维护访问顺序。
两种顺序模式
- 插入顺序(默认) :
accessOrder = false,遍历顺序与 key 的插入顺序一致,重新插入已存在的 key 不改变顺序 - 访问顺序 :
accessOrder = true,每次get/put访问元素后,会将该元素移到双向链表尾部,实现「最近访问的在后面」
经典应用:LRU 缓存
基于访问顺序模式,只需重写removeEldestEntry()方法即可实现简易 LRU 缓存:
java
public class LRUCache<K,V> extends LinkedHashMap<K,V> {
private final int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75f, true); // 开启访问顺序模式
this.capacity = capacity;
}
// 超过容量时淘汰最久未访问的头部元素
@Override
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return size() > capacity;
}
}
3. TreeMap
底层数据结构
红黑树(自平衡二叉搜索树),每个节点包含 key、value、左右子节点、父节点、颜色标识。
核心特性
- 排序有序:key 按自然排序或自定义比较器排序
- key 约束:必须实现
Comparable接口或构造时传入Comparator;不允许 null key - 高级功能:支持范围查询
subMap()/headMap()/tailMap(),支持获取最大 / 最小 key - 增删查均为 O (logn),通过旋转、变色维持树的平衡
- 适用于需要对 key 排序、需要范围查询的场景
4. Hashtable
底层数据结构
数组 + 单向链表,结构与 JDK1.7 的 HashMap 类似。
核心特性
- 几乎所有方法都加
synchronized,锁整个 Hashtable 对象,线程安全但性能极低 - key 和 value 都不允许为 null
- 默认容量 11,扩容为 2 倍 + 1
- JDK1.0 遗留类,官方早已不推荐使用
5. ConcurrentHashMap
底层数据结构
- JDK1.7:分段锁(Segment 数组 + 哈希表数组 + 链表)
- JDK1.8:数组 + 链表 + 红黑树,采用 CAS + synchronized 细粒度锁
核心特性总结
- JDK1.8 锁粒度细化到单个桶,空桶用 CAS 插入,有元素的桶锁头节点
- 支持多线程协助扩容,大幅提升扩容效率
- 采用 baseCount + CounterCell 分散计数,size () 为弱一致近似值
- key 和 value 都不允许为 null
- 并发键值对存储的首选,详细原理可参考 ConcurrentHashMap 专题文章
6. ConcurrentSkipListMap(并发有序)
底层数据结构
跳表(Skip List),一种多层索引的有序链表结构:
- 最底层是完整的有序链表,包含所有数据节点
- 上层是下层的索引层,节点数量逐层减少
- 查询时从顶层索引开始,逐层向下定位,跳过大量节点,达到类似二分查找的效率
为什么并发有序 Map 用跳表不用红黑树?
红黑树的插入删除需要旋转、变色,影响范围大,并发加锁复杂;而跳表的修改仅影响局部相邻节点,更容易实现细粒度并发控制,且范围查询更方便。
特性与适用场景
- 线程安全,并发性能优于
Collections.synchronizedSortedMap - key 排序有序,支持并发场景下的范围查询
- 增删查平均 O (logn)
- key 不允许为 null
- 适用于高并发环境下需要有序键值对、范围查询的场景
Map 六大实现完整对比
| 特性 | HashMap | LinkedHashMap | TreeMap | Hashtable | ConcurrentHashMap | ConcurrentSkipListMap |
|---|---|---|---|---|---|---|
| 底层结构 | 数组 + 链表 + 红黑树 | HashMap + 双向链表 | 红黑树 | 数组 + 链表 | CAS+synchronized + 红黑树 | 跳表 |
| 顺序性 | 无序 | 插入 / 访问有序 | 排序有序 | 无序 | 无序 | 排序有序 |
| null 键值 | 1 个 null 键,多 null 值 | 同 HashMap | key 不能为 null | 都不能为 null | 都不能为 null | key 不能为 null |
| 线程安全 | 不安全 | 不安全 | 不安全 | 安全,全表锁 | 安全,细粒度锁 | 安全,跳表并发 |
| 平均时间复杂度 | O(1) | O(1) | O(logn) | O(1) | O(1) | O(logn) |
| 性能 | 单线程最好 | 略低 | 中等 | 最差 | 高并发下最优 | 较好 |
| 适用场景 | 通用首选 | 保序、LRU 缓存 | 排序、范围查询 | 不推荐 | 并发通用 | 并发有序 + 范围查询 |
五、Queue / Deque 集合:队列与栈
这部分是集合体系中最容易被忽略但面试高频的分支,包含普通队列、双端队列、优先级队列、阻塞队列、非阻塞并发队列等多种实现。
1. ArrayDeque
底层数据结构
循环数组,核心字段:
Object[] elements:存储元素的数组,容量始终是 2 的幂head:头指针,指向队首元素的下标tail:尾指针,指向队尾下一个空位的下标
核心实现原理
- 通过头尾指针的移动实现元素增删,不需要移动数组元素
- 头尾指针越过数组边界时自动绕回另一端,形成循环
- 容量不足时自动扩容为原来的 2 倍
- 同时实现
Deque接口,支持双端操作,既可以当队列也可以当栈
为什么比 LinkedList 快?
- 数组内存连续,CPU 缓存友好
- 首尾增删仅需移动指针,不需要创建销毁节点对象
- 没有节点对象的额外内存开销
适用场景
实现栈、双端队列、普通队列的首选,Java 官方推荐替代古老的 Stack 类和 LinkedList。
2. PriorityQueue 优先级队列
底层数据结构
二叉堆(完全二叉树,用数组存储),默认是小顶堆(堆顶元素最小)。
完全二叉树的数组映射规则:
- 下标为
i的节点,左孩子下标为2*i+1,右孩子为2*i+2 - 父节点下标为
(i-1) / 2
核心实现原理
- 入队(offer) :元素加到数组末尾,执行上浮操作:与父节点比较,比父节点小就交换位置,直到满足堆的性质
- 出队(poll) :取出堆顶元素,将数组最后一个元素移到堆顶,执行下沉操作:与左右孩子比较,和较小的孩子交换,直到满足堆的性质
- 排序依据 :元素实现
Comparable接口,或构造时传入自定义Comparator - 扩容机制:容量小于 64 时扩容 2 倍;大于等于 64 时扩容 1.5 倍
特性与适用场景
- 不保证整体有序,仅保证堆顶是最值
- 不支持按索引随机访问
- 线程不安全,不允许 null 元素
- 适用于 TopK 问题、任务调度、定时器等需要按优先级处理元素的场景
3. 阻塞队列家族(并发)
阻塞队列是生产者 - 消费者模式的核心组件,支持「队满时入队阻塞、队空时出队阻塞」。
ArrayBlockingQueue
- 底层:数组 + 一把 ReentrantLock + 两个 Condition(notEmpty、notFull)
- 特性:有界队列,必须指定容量,初始化后不可扩容;单锁设计,入队和出队不能同时进行
- 适用:容量固定的生产者消费者场景
LinkedBlockingQueue
- 底层:单向链表 + 两把锁(takeLock、putLock)
- 特性:可配置有界 / 无界,默认近似无界(Integer.MAX_VALUE);双锁设计,入队和出队互不阻塞,并发性能更高
- 适用:绝大多数生产者消费者场景,性能优于 ArrayBlockingQueue
SynchronousQueue
- 底层:无缓冲队列,不存储任何元素
- 特性:入队必须等待另一个线程来出队,出队必须等待另一个线程来入队,元素直接交接,不经过缓冲区
- 适用:线程池 CachedThreadPool 的核心实现,适合任务直接交接的场景
PriorityBlockingQueue
- 底层:二叉堆 + 一把锁,无界队列
- 特性:并发版本的优先级队列,出队按优先级排序
- 适用:并发环境下的优先级任务调度
4. ConcurrentLinkedQueue(非阻塞并发队列)
底层数据结构
单向链表 + CAS 无锁算法。
核心实现原理
- 不使用锁,完全通过 CAS 操作实现并发安全
- 无界队列,容量仅受内存限制
- 采用延迟更新头尾节点的优化设计,减少 CAS 竞争次数
特性与适用场景
- 非阻塞,高并发下性能优于阻塞队列
- 不支持阻塞等待
- size () 返回近似值,弱一致性
- 适用于高并发下不需要阻塞等待的非阻塞队列场景
常用队列完整对比
| 队列实现 | 底层结构 | 有界 / 无界 | 线程安全 | 阻塞特性 | 排序特性 | 适用场景 |
|---|---|---|---|---|---|---|
| ArrayDeque | 循环数组 | 无界 | 不安全 | 不阻塞 | 插入有序 | 栈、双端队列,单线程 |
| PriorityQueue | 二叉堆 | 无界 | 不安全 | 不阻塞 | 优先级排序 | 优先级任务,单线程 |
| ArrayBlockingQueue | 数组 + 单锁 | 有界 | 安全 | 阻塞 | 插入有序 | 固定容量生产者消费者 |
| LinkedBlockingQueue | 链表 + 双锁 | 有界 / 无界 | 安全 | 阻塞 | 插入有序 | 通用生产者消费者 |
| SynchronousQueue | 无缓冲 | 0 容量 | 安全 | 阻塞 | 无 | 任务直接交接 |
| PriorityBlockingQueue | 二叉堆 + 锁 | 无界 | 安全 | 阻塞 | 优先级排序 | 并发优先级任务 |
| ConcurrentLinkedQueue | 链表 + CAS | 无界 | 安全 | 不阻塞 | 插入有序 | 高并发非阻塞队列 |
六、面试高频核心考点
1. ArrayList 和 LinkedList 的本质区别?
本质是数组和链表两种数据结构的差异:
- 内存布局:数组是连续内存,链表是离散节点
- 访问性能:数组随机访问 O (1),链表必须遍历 O (n)
- 增删性能:数组中间增删需要移动元素;链表仅需改指针,但需要先遍历定位
- 缓存友好:数组连续内存 CPU 缓存命中率高,链表缓存不友好
绝大多数场景都应该用 ArrayList,仅在频繁头尾增删且极少随机访问时考虑 LinkedList;实现栈 / 队列优先选 ArrayDeque。
2. PriorityQueue 的底层原理是什么?如何保证有序?
PriorityQueue 底层是二叉堆(数组实现的完全二叉树),默认小顶堆。
- 它不保证整体有序,仅保证堆顶元素是最值
- 入队执行上浮操作,出队执行下沉操作,维持堆的性质
- 想要得到全量有序结果,需要逐个 poll 出队
3. 写时复制(COW)的原理与优缺点?
原理:读操作直接读取原数组,完全无锁;写操作加独占锁,复制一份新数组,修改完成后替换数组引用。
- 优点:读性能极高,线程安全,迭代不会抛出并发修改异常
- 缺点:写操作开销大,内存占用高;数据弱一致性;写频繁时性能极差
- 适用:读多写少、数据更新不频繁的场景
4. 跳表和红黑树有什么区别?为什么并发有序 Map 用跳表?
表格
| 维度 | 跳表 | 红黑树 |
|---|---|---|
| 数据结构 | 多层索引有序链表 | 自平衡二叉搜索树 |
| 查找效率 | 平均 O (logn) | 稳定 O (logn) |
| 实现复杂度 | 简单 | 复杂 |
| 范围查询 | 方便,直接遍历链表 | 麻烦,需要中序遍历 |
| 并发友好度 | 高,修改仅影响局部节点 | 低,旋转影响范围大 |
并发有序集合用跳表不用红黑树,核心原因是跳表修改仅影响局部节点,更容易实现细粒度并发控制,并发性能更好,且范围查询更便捷。
5. fail-fast 和 fail-safe 的区别?
- fail-fast(快速失败) :普通非并发集合采用。迭代时维护
expectedModCount,检测到集合结构被修改则立即抛出ConcurrentModificationException,目的是快速暴露并发修改问题。 - fail-safe(安全失败):并发集合采用。迭代基于数据快照,不会抛出异常,但迭代器看不到最新数据,是弱一致性的。
6. 为什么不推荐 Vector 和 Hashtable?
- 锁粒度太粗,全对象加锁,所有方法串行执行,并发性能极低
- 属于 JDK1.0 遗留产物,设计老旧,官方早已不推荐
- 即使加锁,复合操作也不是原子的,仍然存在并发问题
- 有更优的替代方案:单线程用普通集合,并发场景用并发集合
七、集合选型最佳实践
- 普通列表存储:优先选 ArrayList,90% 以上场景的最优解
- 栈 / 双端队列:选 ArrayDeque,不要用 Stack 和 LinkedList
- 通用去重:优先选 HashSet,性能最好
- 去重 + 保插入顺序:选 LinkedHashSet
- 去重 + 排序 / 范围查询:选 TreeSet
- 普通键值对:优先选 HashMap
- 键值对 + 排序 / 范围查询:选 TreeMap
- 并发键值对:选 ConcurrentHashMap,绝对不用 Hashtable
- 并发有序键值对:选 ConcurrentSkipListMap
- 并发读多写少列表:选 CopyOnWriteArrayList
- 生产者消费者模式:优先选 LinkedBlockingQueue,固定容量选 ArrayBlockingQueue
- 优先级任务:单线程用 PriorityQueue,并发用 PriorityBlockingQueue
八、总结
Java 集合框架的选型,本质上是数据结构的选型。每一种集合都对应一种经典数据结构,有其擅长的场景和固有的短板。
理解底层数据结构,才能从本质上区分不同集合的差异,在合适的场景选对合适的工具,既保证性能,又能避开各种隐性坑点;面试时也能从原理层面讲清设计逻辑,而不只是背诵 API 和表面特性。