Appearance
Java 集合详解
Java 集合既是业务代码里最常用的基础设施之一,也是理解 Java 数据组织方式时绕不开的一块。
很多人一开始会把集合记成几个类名:
ArrayListLinkedListHashSetHashMap
只记住这些类名当然不够,继续往下看时,更重要的是把下面这些问题理顺:
- 它们分别属于什么集合类型
- 各自的特点是什么
- 底层是怎么实现的
- 适合什么使用场景
- 有哪些常见坑和注意事项
这篇文章就按这个顺序,把 Java 集合体系完整整理一遍。
1. 先建立整体认识:Collection 和 Map 不是一回事
Java 集合框架可以粗分成两大体系:
CollectionMap
它们的关系不是上下级包含,而是并列的两条线。
1.1 Collection
Collection 更偏“单值集合”,也就是一组元素的管理。
它下面最常见的三类是:
ListSetQueue / Deque
1.2 Map
Map 更偏“键值对集合”,核心是 key -> value 的映射关系。
所以最基础的框架图可以记成:
text
Collection
├── List
├── Set
└── Queue / Deque
Map如果想再往下看得更完整一点,可以记这张关系树:
text
Java Collections Framework
├── Collection
│ ├── List
│ │ ├── ArrayList
│ │ ├── LinkedList
│ │ ├── Vector
│ │ └── Stack
│ ├── Set
│ │ ├── HashSet
│ │ ├── LinkedHashSet
│ │ ├── SortedSet
│ │ │ └── NavigableSet
│ │ │ └── TreeSet
│ │ └── EnumSet
│ └── Queue
│ ├── Deque
│ │ ├── ArrayDeque
│ │ └── LinkedList
│ └── PriorityQueue
└── Map
├── HashMap
│ └── LinkedHashMap
├── Hashtable
│ └── Properties
├── SortedMap
│ └── NavigableMap
│ └── TreeMap
├── WeakHashMap
├── IdentityHashMap
├── EnumMap
└── ConcurrentMap
└── ConcurrentHashMap这张树最重要的不是一次把所有类名都背下来,而是先建立两个认知:
Collection和Map是并列主线,不是包含关系List / Set / Queue属于Collection体系,而HashMap / TreeMap / ConcurrentHashMap属于Map体系
如果只想先记最常用的一层,也可以压缩成:
text
Collection
├── List
├── Set
└── Queue
Map
├── HashMap
├── LinkedHashMap
├── TreeMap
└── ConcurrentHashMap1.3 两张总表:把 Collection 和 Map 的常见实现看清楚
如果你不想一开始就陷入每个类的底层细节,最稳妥的方式是看两张总表。
先记一个大前提:
Collection体系的共同点是:都在管理“一组单值元素”Map体系的共同点是:都在管理“key -> value映射关系”
真正拉开差异的,通常是:
- 是否允许重复
- 是否保留顺序
- 是否支持排序
- 底层结构是什么
- 是否线程安全
- 更适合什么业务场景
1.3.1 Collection 常见实现对比表
| 分类 | 实现类 | 共同点 | 不同点 | 适用场景 | 线程安全 | null 支持 |
|---|---|---|---|---|---|---|
List | ArrayList | 都属于 Collection 体系,管理单值元素 | 动态数组;有序、可重复;按下标访问快;中间插删慢 | 读多写少、频繁遍历、按索引访问 | 否 | 允许 |
List | LinkedList | 都属于 Collection 体系,管理单值元素 | 双向链表;有序、可重复;头尾插删灵活;随机访问慢 | 频繁头尾操作、同时想当 Deque 用 | 否 | 允许 |
List | Vector | 都属于 Collection 体系,管理单值元素 | 动态数组;方法大多自带同步;历史实现色彩更重 | 老代码兼容、了解历史线程安全列表 | 是,但粒度较粗 | 允许 |
List | CopyOnWriteArrayList | 都属于 Collection 体系,管理单值元素 | 写时复制;读多写少友好;写入成本高;迭代读快照 | 监听器、配置列表、白名单等读多写少场景 | 是 | 允许 |
Set | HashSet | 都属于 Collection 体系,强调单值元素唯一性 | 基于哈希;无序;去重依赖 hashCode / equals | 去重、判断元素是否存在 | 否 | 允许一个 |
Set | LinkedHashSet | 都属于 Collection 体系,强调单值元素唯一性 | 在哈希基础上维护插入顺序 | 去重且要稳定输出顺序 | 否 | 允许一个 |
Set | TreeSet | 都属于 Collection 体系,强调单值元素唯一性 | 基于红黑树;自动排序;支持范围查询 | 排序去重、最值和区间场景 | 否 | 一般不建议放,比较时容易出问题 |
Set | CopyOnWriteArraySet | 都属于 Collection 体系,强调单值元素唯一性 | 写时复制;读多写少友好;迭代读快照 | 监听器集合、白名单、配置开关集合 | 是 | 允许一个 |
Set | ConcurrentSkipListSet | 都属于 Collection 体系,强调单值元素唯一性 | 基于跳表;线程安全;元素有序;支持范围查询 | 并发有序去重、区间查询 | 是 | 不允许 |
Queue / Deque | ArrayDeque | 都属于 Collection 体系,强调元素按特定语义取用 | 循环数组;头尾操作高效;可作队列也可作栈 | 普通队列、双端队列、栈替代 Stack | 否 | 不允许 |
Queue | PriorityQueue | 都属于 Collection 体系,强调元素按特定语义取用 | 基于堆;不是 FIFO;队头优先级最高 | Top K、调度、优先级任务 | 否 | 不允许 |
| 并发队列 | BlockingQueue | 都属于 Collection 体系,强调元素按特定语义取用 | 支持阻塞式生产消费;常见有界/无界实现 | 生产者消费者、线程池任务队列 | 是,具体看实现 | 通常不允许 |
读这张表时,最值得先记住的是:
- 普通列表优先想
ArrayList - 去重优先想
HashSet - 映射关系不在这张表里,而在
Map那张表里 - 队列和双端队列优先想
ArrayDeque - 并发读多写少的列表才优先考虑
CopyOnWriteArrayList - 线程安全且读多写少的
Set可以优先考虑CopyOnWriteArraySet - 线程安全且还要有序的
Set可以考虑ConcurrentSkipListSet
1.3.2 Map 常见实现对比表
| 实现类 | 共同点 | 不同点 | 适用场景 | 线程安全 | 顺序特性 | null 支持 |
|---|---|---|---|---|---|---|
HashMap | 都属于 Map 体系,管理 key -> value 映射 | 哈希结构;查询快;最常用;无顺序保证 | 普通键值映射、计数、状态表 | 否 | 不保证稳定顺序 | 允许一个 null key,允许多个 null value |
LinkedHashMap | 都属于 Map 体系,管理 key -> value 映射 | 在 HashMap 基础上维护插入顺序或访问顺序 | 既要快速查找,又要稳定遍历顺序;简单 LRU | 否 | 保留插入顺序,或按访问顺序维护 | 允许 |
TreeMap | 都属于 Map 体系,管理 key -> value 映射 | 基于红黑树;按 key 排序;支持范围查询 | 排序映射、区间查找、最值查找 | 否 | 按 key 有序 | 通常不允许 null key |
Hashtable | 都属于 Map 体系,管理 key -> value 映射 | 老的同步实现;方法大多直接加锁 | 主要用于理解历史代码 | 是,但粒度较粗 | 无稳定业务顺序语义 | 不允许 null key,不允许 null value |
ConcurrentHashMap | 都属于 Map 体系,管理 key -> value 映射 | 并发安全;性能明显优于粗粒度整表锁;支持并发读写 | 并发缓存、共享状态表、高并发映射 | 是 | 无稳定业务顺序语义 | 不允许 null key,不允许 null value |
EnumMap | 都属于 Map 体系,管理 key -> value 映射 | key 必须是枚举;结构紧凑;性能通常很好 | 枚举状态映射、固定类别映射 | 否 | 按枚举定义顺序遍历 | 不允许 null key |
WeakHashMap | 都属于 Map 体系,管理 key -> value 映射 | key 是弱引用;可能被 GC 自动回收 | 缓存、规范化映射等弱引用场景 | 否 | 无稳定业务顺序语义 | 允许 null key |
IdentityHashMap | 都属于 Map 体系,管理 key -> value 映射 | 判断 key 相等性时更看重引用身份而不是 equals | 需要按对象身份区分 key 的特殊场景 | 否 | 无稳定业务顺序语义 | 允许 |
读这张表时,最值得先记住的是:
- 普通映射优先想
HashMap - 要顺序就想
LinkedHashMap - 要排序就想
TreeMap - 并发场景优先想
ConcurrentHashMap EnumMap、WeakHashMap、IdentityHashMap都是偏特殊语义场景
1.4 Collection 和 Collections 的区别
这是很容易混淆的一道题。
Collection是集合顶层接口Collections是工具类
Collections 里提供的是:
- 排序
- 查找
- 同步包装
- 不可变集合包装
所以不要把它们当成一回事。
2. List:有序、可重复、可按下标访问
List 的核心特点是:
- 元素有序
- 允许重复
- 支持按索引访问
最常见的实现类有:
ArrayListLinkedListVectorCopyOnWriteArrayList
2.1 ArrayList
ArrayList 是业务代码里最常用的 List 实现。
特点
- 底层是动态数组
- 支持快速按索引访问
- 尾部追加性能通常较好
- 非线程安全
底层实现
可以把它理解成:一个会自动扩容的数组。
核心点包括:
- 内部用数组存元素
- 容量不够时会扩容
- 扩容通常是按原容量的
1.5倍左右增长 - 中间位置插入或删除时,需要搬移后面的元素
如果把视角再往底层压一层,ArrayList 的核心状态其实很少:
- 一个真正存元素的数组
- 一个表示当前元素个数的
size
最值得抓住的结构可以简化成:
java
/**
* ArrayList 的核心状态示意。
*/
public class ArrayList<E> {
/**
* 真正存元素的底层数组。
*/
transient Object[] elementData;
/**
* 当前已经存了多少个元素。
*/
private int size;
}这也是为什么:
get(index)很快,因为本质上就是数组下标访问- 尾部追加通常比较快,因为多数时候只是把元素写到
elementData[size] - 中间插入和删除比较慢,因为后面的元素要整体搬移
例如 add(E e) 的核心流程,压缩后大致就是这样:
java
/**
* ArrayList 尾部追加元素的核心流程示意。
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size] = e;
size++;
return true;
}这里的关键不是代码本身有多复杂,而是要建立这个直觉:多数 add 本质上就是数组尾部写入;只有容量不够时,才会触发扩容和数组复制。
扩容时常见会走到类似下面这段逻辑:
java
/**
* ArrayList 扩容的核心思路示意。
*/
private Object[] grow(Object[] oldArray, int minCapacity) {
int oldCapacity = oldArray.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity < minCapacity) {
newCapacity = minCapacity;
}
return Arrays.copyOf(oldArray, newCapacity);
}这里最值得记住的是:
- 扩容不是每次都发生
- 一旦扩容,底层会复制旧数组
- 这也是为什么已知数据量时,提前指定容量往往更稳妥
使用场景
适合:
- 读多写少
- 频繁遍历
- 按下标访问
- 大多数普通业务列表场景
注意事项
- 中间插入和删除成本高,因为要搬移元素
- 扩容会带来数组复制开销
- 多线程下不能直接安全共享
- 如果已知数据量,最好指定初始容量,减少扩容次数
例如:
java
List<User> users = new ArrayList<>(1024);2.2 LinkedList
LinkedList 大家都很熟,但实际业务里出现频率通常没 ArrayList 高。
特点
- 底层是双向链表
- 插入和删除节点本身比较灵活
- 同时实现了
List和Deque - 非线程安全
底层实现
它的每个节点通常会保存:
- 当前元素
- 前驱节点引用
- 后继节点引用
所以它更像:一串前后相连的节点。
如果继续往下看,LinkedList 的核心不是“链表”这三个字,而是:
- 它是双向链表
- 它单独保存了
first和last - 头尾插入删除时,通常只需要修改少量引用
压缩后的核心结构可以看成这样:
java
/**
* LinkedList 的节点结构示意。
*/
private static class Node<E> {
/**
* 当前节点保存的元素。
*/
E item;
/**
* 前驱节点。
*/
Node<E> prev;
/**
* 后继节点。
*/
Node<E> next;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.prev = prev;
this.next = next;
}
}链表本身通常还会保存:
first:头节点last:尾节点size:当前节点数量
这就是为什么它的头尾插入删除很自然。
比如 linkFirst 的核心流程,压缩后大致是这样:
java
/**
* LinkedList 头部插入的核心流程示意。
*/
private void linkFirst(E e) {
Node<E> oldFirst = first;
Node<E> newNode = new Node<>(null, e, oldFirst);
first = newNode;
if (oldFirst == null) {
last = newNode;
} else {
oldFirst.prev = newNode;
}
size++;
}从这段代码最应该读出来的是:
- 头插并不需要搬移整批元素
- 大多数情况下只是创建一个新节点,然后改几条引用
- 这也是为什么说它“头尾插入删除快”
但这句话有一个很重要的前提:你操作的是头部或尾部,或者你已经拿到了目标节点。
如果你是按下标访问,例如 get(index),核心流程通常会像这样:
java
/**
* LinkedList 按下标查找节点的核心流程示意。
*/
Node<E> node(int index) {
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++) {
x = x.next;
}
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--) {
x = x.prev;
}
return x;
}
}这也是为什么:
LinkedList理论上插删友好- 但随机访问通常明显慢于
ArrayList - 普通业务里如果不是频繁头尾操作,很多时候还是
ArrayList更常见
使用场景
适合:
- 需要频繁在头尾插入删除
- 既想当
List用,也想当队列 / 双端队列用
注意事项
- 按索引访问性能差,因为需要沿链表查找
- 节点对象多,内存开销比
ArrayList更大 - 即使插入删除理论上更友好,前提也是你已经定位到目标节点
- 普通业务开发里,大多数场景仍然优先考虑
ArrayList
2.3 Vector
Vector 是比较老的集合实现。
特点
- 底层也是动态数组
- 方法大多带同步
- 线程安全粒度比较粗
使用场景
现在一般不作为首选,更多是历史知识。
如果你需要线程安全的列表,更常见的替代方案是:
Collections.synchronizedListCopyOnWriteArrayList- 更高层的并发控制
2.4 CopyOnWriteArrayList
这是并发场景下比较典型的列表实现。
特点
- 读操作基本不加锁
- 写操作会复制底层数组
- 读性能好,写成本高
- 迭代时看到的是快照
底层实现
核心思想是:写时复制。
也就是修改时不直接在原数组上改,而是复制出新数组再替换引用。
如果继续往下拆,CopyOnWriteArrayList 的线程安全关键不在于“神奇”,而在于:
- 读和写走的是两条不同路径
- 写操作会加锁
- 写完后用新数组整体替换旧数组
- 底层数组引用是
volatile,新结果能被别的线程及时看见
它的核心状态可以看成这样:
java
import java.util.concurrent.locks.ReentrantLock;
/**
* CopyOnWriteArrayList 的核心状态示意。
*/
public class CopyOnWriteArrayList<E> {
/**
* 写操作使用的独占锁。
*/
final transient ReentrantLock lock = new ReentrantLock();
/**
* 当前生效的底层数组。
* 使用 volatile 是为了让新数组引用对其他线程可见。
*/
private transient volatile Object[] array;
}读操作为什么几乎不用加锁,核心原因就在这里:读线程拿到的是某一时刻已经发布好的数组快照。
例如 get(int index) 的核心思路,压缩后大致就是:
java
/**
* CopyOnWriteArrayList 读操作的核心流程示意。
*/
public E get(int index) {
Object[] snapshot = array;
return (E) snapshot[index];
}这里最值得记住的是:
- 读线程直接读当前数组引用
- 不在原数组上做修改
- 所以读操作可以很轻
而写操作会走另一条路。以 add(E e) 为例,压缩后的核心流程大致如下:
java
/**
* CopyOnWriteArrayList 写操作的核心流程示意。
*/
public boolean add(E e) {
lock.lock();
try {
Object[] oldArray = array;
int len = oldArray.length;
Object[] newArray = Arrays.copyOf(oldArray, len + 1);
newArray[len] = e;
array = newArray;
return true;
} finally {
lock.unlock();
}
}从这段代码最应该读出来的是:
- 写操作不是在旧数组上原地改
- 而是先复制,再写新数组,再整体替换引用
- 旧数组仍然可以继续被正在读的线程安全使用
这也是为什么它线程安全,而且读多写少场景表现很好:
- 读线程彼此几乎不互相阻塞
- 读线程也不会和写线程争同一份可变数组
- 代价是每次写都要复制数组,写得越频繁,成本越高
使用场景
适合:
- 读多写少
- 配置列表
- 监听器列表
- 白名单、黑名单等更新不频繁但读取频繁的场景
注意事项
- 写入开销高
- 内存占用会放大
- 不能期待它适合高频写入
- 迭代读到的是快照,不一定是实时最新数据
2.5 一组最常见的 List 用法示例
如果你想把 List 相关实现类的日常用法一次看清,可以看下面这组示例。
这组代码重点演示:
- 新增元素
- 按位置插入
- 查询元素
- 删除元素
- 不同
List实现更典型的使用方式
java
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 演示几种常见 List 实现的基本增删查用法。
*/
public class ListUsageDemo {
public static void main(String[] args) {
useArrayList();
useLinkedList();
useVector();
useCopyOnWriteArrayList();
}
/**
* 演示 ArrayList 的典型用法:
* 尾部追加、按下标访问、按位置插入、按值删除。
*/
private static void useArrayList() {
List<String> todoList = new ArrayList<>();
todoList.add("write-outline");
todoList.add("review-draft");
todoList.add(1, "collect-feedback");
String firstTask = todoList.get(0);
todoList.remove("review-draft");
boolean containsFeedbackTask = todoList.contains("collect-feedback");
System.out.println("ArrayList => " + todoList);
System.out.println("firstTask => " + firstTask);
System.out.println("containsFeedbackTask => " + containsFeedbackTask);
}
/**
* 演示 LinkedList 作为双端列表的典型用法:
* 头尾插入、头尾删除、读取首尾元素。
*/
private static void useLinkedList() {
LinkedList<String> requestSteps = new LinkedList<>();
requestSteps.addFirst("receive-request");
requestSteps.addLast("query-database");
requestSteps.addLast("build-response");
String firstStep = requestSteps.getFirst();
String lastStep = requestSteps.removeLast();
System.out.println("LinkedList => " + requestSteps);
System.out.println("firstStep => " + firstStep);
System.out.println("removedLastStep => " + lastStep);
}
/**
* 演示 Vector 的基本用法。
* 这里只是帮助理解老的同步 List API,不代表新代码优先使用它。
*/
private static void useVector() {
Vector<String> legacyConfigs = new Vector<>();
legacyConfigs.add("legacy-timeout");
legacyConfigs.add("legacy-retry");
String config = legacyConfigs.get(0);
legacyConfigs.remove("legacy-timeout");
System.out.println("Vector => " + legacyConfigs);
System.out.println("firstConfig => " + config);
}
/**
* 演示 CopyOnWriteArrayList 在读多写少场景下的基本用法。
*/
private static void useCopyOnWriteArrayList() {
CopyOnWriteArrayList<String> listenerList = new CopyOnWriteArrayList<>();
listenerList.add("email-listener");
listenerList.add("audit-listener");
listenerList.remove("email-listener");
boolean containsAuditListener = listenerList.contains("audit-listener");
System.out.println("CopyOnWriteArrayList => " + listenerList);
System.out.println("containsAuditListener => " + containsAuditListener);
}
}3. Set:无重复
Set 的核心特点只有一句话:元素不能重复。
但“无重复”背后具体怎么实现,要看不同实现类。
最常见的有:
HashSetLinkedHashSetTreeSet
3.1 HashSet
特点
- 元素无序
- 不允许重复
- 允许一个
null - 非线程安全
底层实现
HashSet 底层其实是基于 HashMap 实现的。
你可以把它理解成:只用 key,不关心 value 的 HashMap。
也就是说:
- 元素作为
key value只是一个固定占位对象
压缩后的核心结构和插入流程,大致可以理解成这样:
java
/**
* HashSet 底层基于 HashMap 的核心思路示意。
*/
public class HashSet<E> {
/**
* 真正负责存储元素的底层 HashMap。
*/
private transient HashMap<E, Object> map;
/**
* 所有元素共用的占位 value。
*/
private static final Object PRESENT = new Object();
/**
* HashSet.add 的核心流程。
* 元素会作为 key 放进 map。
*/
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
}这里最值得记住的是:
HashSet的“去重”能力,本质上是HashMap key不能重复- 元素能不能正确去重,最终还是取决于
hashCode()和equals() - 所以
HashSet的很多底层特征,其实都跟HashMap高度一致
使用场景
适合:
- 去重
- 判断元素是否存在
- 对顺序没有要求的唯一性集合
注意事项
- 去重依赖
hashCode()和equals() - 如果元素参与哈希计算的字段会变,可能导致查找异常
- 不保证遍历顺序稳定
3.2 LinkedHashSet
特点
- 不允许重复
- 保留插入顺序
- 查询复杂度通常仍然接近哈希结构
底层实现
底层基于 LinkedHashMap。
所以它既有哈希表定位能力,也额外维护了顺序链。
也就是说,它并不是在 HashSet 外面单独再包一层顺序结构,而是:
- 底层仍然用哈希结构做定位
- 再通过双向链表维护插入顺序
这也是为什么它能同时做到:
- 去重
- 查询效率仍然接近哈希结构
- 遍历结果保持稳定顺序
使用场景
适合:
- 既要去重
- 又希望保留插入顺序
例如:
- 标签去重后仍想按用户原始输入顺序展示
- 去重后的结果还要稳定输出
3.3 TreeSet
特点
- 不允许重复
- 元素有序
- 默认按自然顺序排序,或按比较器排序
底层实现
底层基于 TreeMap,核心结构通常是红黑树。
所以它擅长的是:
- 有序存储
- 范围查找
- 自动排序
从实现关系上看,TreeSet 和 HashSet 很像,也是在“借底层 Map 的能力”。
压缩后的核心思路可以理解成:
java
/**
* TreeSet 基于 TreeMap 的核心思路示意。
*/
public class TreeSet<E> {
/**
* 真正负责存储元素的底层 TreeMap。
*/
private transient NavigableMap<E, Object> m;
private static final Object PRESENT = new Object();
/**
* TreeSet.add 的核心流程。
* 元素会作为有序映射的 key 放进去。
*/
public boolean add(E e) {
return m.put(e, PRESENT) == null;
}
}这里最关键的区别在于:
HashSet借的是HashMapTreeSet借的是TreeMap- 所以一个更强调哈希去重,一个更强调排序去重
使用场景
适合:
- 需要排序去重
- 需要取最小值、最大值
- 需要范围查询
注意事项
- 元素必须可比较,或者显式传入
Comparator - 排序规则要和“相等性”判断保持一致,否则会出现逻辑混乱
- 性能通常不如基于哈希的
HashSet
3.4 线程安全的 Set
Set 这条主线里,一个很容易被遗漏的问题是:如果多个线程要同时读写同一份去重集合,应该选什么?
Set 接口本身并没有一个像 HashSet 那样默认最常用的并发实现,通常要根据读写模型来选。
最常见的几种方案可以记成这张表:
| 方案 | 核心特点 | 更适合什么场景 | 需要注意什么 |
|---|---|---|---|
Collections.synchronizedSet(new HashSet<>()) | 用同步包装普通 Set | 并发不高、改造老代码 | 迭代时通常仍要手动同步 |
CopyOnWriteArraySet | 写时复制;读线程读快照 | 读多写少、监听器、白名单 | 写入成本高,不适合高频写 |
ConcurrentHashMap.newKeySet() | 基于 ConcurrentHashMap 的 key 视图;无序;并发写友好 | 在线用户、任务去重、共享状态集合 | 迭代是弱一致性视图,不是静态快照 |
ConcurrentSkipListSet | 基于跳表;线程安全;元素有序 | 并发有序去重、范围查询 | 不允许 null,性能取舍和哈希结构不同 |
如果只想先抓最实用的选择方式,可以直接这样记:
- 并发量不高,先求简单:
Collections.synchronizedSet(...) - 读很多、写很少:
CopyOnWriteArraySet - 并发写也很多,而且不要求顺序:
ConcurrentHashMap.newKeySet() - 并发下还要保持有序:
ConcurrentSkipListSet
下面这组代码把最常见的创建方式放在一起,便于建立直觉:
java
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* 演示几种线程安全 Set 的典型创建方式。
*/
public class ConcurrentSetUsageDemo {
public static void main(String[] args) {
useSynchronizedSet();
useCopyOnWriteArraySet();
useConcurrentHashMapKeySet();
useConcurrentSkipListSet();
}
/**
* 演示同步包装版 Set。
* 适合并发量不高、优先求简单的场景。
*/
private static void useSynchronizedSet() {
Set<String> syncTags = Collections.synchronizedSet(new HashSet<>());
syncTags.add("java");
syncTags.add("redis");
syncTags.remove("redis");
System.out.println("synchronizedSet => " + syncTags);
}
/**
* 演示 CopyOnWriteArraySet。
* 适合读多写少的监听器和配置集合。
*/
private static void useCopyOnWriteArraySet() {
CopyOnWriteArraySet<String> listenerSet = new CopyOnWriteArraySet<>();
listenerSet.add("email-listener");
listenerSet.add("audit-listener");
listenerSet.remove("email-listener");
System.out.println("CopyOnWriteArraySet => " + listenerSet);
}
/**
* 演示基于 ConcurrentHashMap 的并发无序 Set。
* 适合高并发去重和共享状态标记。
*/
private static void useConcurrentHashMapKeySet() {
Set<String> onlineUsers = ConcurrentHashMap.newKeySet();
onlineUsers.add("alice");
onlineUsers.add("bob");
onlineUsers.remove("alice");
System.out.println("ConcurrentHashMap.newKeySet => " + onlineUsers);
}
/**
* 演示并发有序 Set。
* 适合一边并发写入,一边按顺序读取的场景。
*/
private static void useConcurrentSkipListSet() {
ConcurrentSkipListSet<Integer> orderedIds = new ConcurrentSkipListSet<>();
orderedIds.add(30);
orderedIds.add(10);
orderedIds.add(20);
orderedIds.remove(20);
System.out.println("ConcurrentSkipListSet => " + orderedIds);
}
}3.5 一组最常见的 Set 用法示例
Set 最关键的主线是“去重”,但不同实现类对顺序和排序的支持不一样。
下面这组代码重点演示:
- 新增元素
- 自动去重
- 判断元素是否存在
- 删除元素
- 插入顺序和排序语义
java
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;
/**
* 演示几种常见 Set 实现的基本用法。
*/
public class SetUsageDemo {
public static void main(String[] args) {
useHashSet();
useLinkedHashSet();
useTreeSet();
}
/**
* 演示 HashSet 的典型用法:
* 去重、判断是否存在、删除元素。
*/
private static void useHashSet() {
Set<String> permissionCodes = new HashSet<>();
permissionCodes.add("product:read");
permissionCodes.add("product:update");
permissionCodes.add("product:read");
boolean canRead = permissionCodes.contains("product:read");
permissionCodes.remove("product:update");
System.out.println("HashSet => " + permissionCodes);
System.out.println("canRead => " + canRead);
}
/**
* 演示 LinkedHashSet 的典型用法:
* 在去重的同时保留插入顺序。
*/
private static void useLinkedHashSet() {
Set<String> orderedTags = new LinkedHashSet<>();
orderedTags.add("java");
orderedTags.add("mysql");
orderedTags.add("redis");
orderedTags.add("java");
orderedTags.remove("mysql");
System.out.println("LinkedHashSet => " + orderedTags);
}
/**
* 演示 TreeSet 的典型用法:
* 自动排序、获取最小值和最大值、删除元素。
*/
private static void useTreeSet() {
TreeSet<Integer> responseTimes = new TreeSet<>();
responseTimes.add(320);
responseTimes.add(120);
responseTimes.add(260);
Integer min = responseTimes.first();
Integer max = responseTimes.last();
responseTimes.remove(260);
System.out.println("TreeSet => " + responseTimes);
System.out.println("min => " + min);
System.out.println("max => " + max);
}
}4. Queue / Deque:更强调操作语义
如果说 List 更像“列表”,那 Queue 更像“按规则取用元素的容器”。
常见类型包括:
QueueDequePriorityQueueArrayDeque
4.1 Queue
Queue 更偏队列语义,通常是:先进先出(FIFO)。
常见操作包括:
offerpollpeek
它适合表达:
- 排队
- 消费
- 调度
4.2 Deque
Deque 是双端队列。
它允许:
- 头部插入和删除
- 尾部插入和删除
所以它既可以当队列,也可以当栈来用。
4.3 ArrayDeque
这是很值得记的一个实现类。
特点
- 底层通常是循环数组
- 头尾操作都比较高效
- 既能当队列,也能当栈
- 不允许存
null
底层实现
ArrayDeque 最值得建立的直觉是:它不是普通线性数组,而是一个首尾相接的循环数组。
它通常会维护:
elements:真正存元素的数组head:当前头部位置tail:下一个尾部写入位置
压缩后的核心结构可以理解成这样:
java
/**
* ArrayDeque 的核心状态示意。
*/
public class ArrayDeque<E> {
/**
* 循环数组本体。
*/
transient Object[] elements;
/**
* 队头下标。
*/
transient int head;
/**
* 队尾下一个可写入的位置。
*/
transient int tail;
}例如尾部插入的核心流程,大致可以压缩成:
java
/**
* ArrayDeque 尾部插入的核心流程示意。
*/
public void addLast(E e) {
elements[tail] = e;
tail = (tail + 1) & (elements.length - 1);
if (tail == head) {
grow();
}
}头部删除则通常类似:
java
/**
* ArrayDeque 头部删除的核心流程示意。
*/
public E pollFirst() {
int h = head;
E result = (E) elements[h];
if (result == null) {
return null;
}
elements[h] = null;
head = (h + 1) & (elements.length - 1);
return result;
}这也是为什么它的头尾操作通常很高效:
- 不需要像
ArrayList那样搬移大量元素 - 大多数时候只是改下标和写数组槽位
- 这也让它非常适合表达队列、栈和双端队列语义
使用场景
适合:
- 普通队列
- 双端队列
- 栈结构替代
Stack
注意事项
- 如果只是想用栈,通常更推荐
ArrayDeque而不是老的Stack - 不能放
null
4.4 PriorityQueue
PriorityQueue 不是普通 FIFO 队列,而是优先级队列。
特点
- 底层通常是堆,常见是小顶堆
- 每次取出的都是优先级最高或最低的元素
- 不保证整体遍历就是完全有序
底层实现
PriorityQueue 的底层重点不是“队列”,而是:它内部通常是用数组表示的一棵二叉堆。
默认场景下更常见的是:小顶堆。
也就是:
- 堆顶元素最小
- 每次
poll()拿到的是当前最小值 - 但整个数组并不是完全排好序的
压缩后的核心结构可以理解成这样:
java
/**
* PriorityQueue 的核心状态示意。
*/
public class PriorityQueue<E> {
/**
* 真正存堆节点的数组。
*/
transient Object[] queue;
/**
* 当前元素个数。
*/
int size;
}插入元素的核心思路通常是:
- 把元素放到数组尾部
- 再一路向上比较
- 必要时和父节点交换位置
压缩后的 offer 核心流程可以看成这样:
java
/**
* PriorityQueue 插入元素的核心流程示意。
*/
public boolean offer(E e) {
int i = size;
size = i + 1;
if (i == 0) {
queue[0] = e;
} else {
siftUp(i, e);
}
return true;
}而删除堆顶元素的核心思路通常是:
- 取出堆顶
- 把最后一个元素放到堆顶
- 再一路向下比较并调整
这也是为什么它非常适合:
- 每次只关心“当前优先级最高的是谁”
- 不要求整个集合随时保持完全有序
- 需要反复取最值的场景
使用场景
适合:
- Top K
- 定时调度
- 最值维护
- 贪心算法和图算法
注意事项
- 它保证的是队头优先,不是整体完全排序
- 如果要完整有序结果,通常需要反复
poll - 元素需要可比较,或提供比较器
4.5 一组最常见的 Queue / Deque 用法示例
队列相关集合最值得先熟悉的是:
offer:放入元素poll:取出并删除队头元素peek:只查看队头元素,不删除- 双端队列的头尾操作
- 优先级队列的优先出队语义
java
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.PriorityQueue;
import java.util.Queue;
/**
* 演示普通队列、双端队列和优先级队列的基本用法。
*/
public class QueueUsageDemo {
public static void main(String[] args) {
useArrayDequeAsQueue();
useArrayDequeAsDeque();
usePriorityQueue();
}
/**
* 演示 ArrayDeque 作为普通 FIFO 队列的用法。
*/
private static void useArrayDequeAsQueue() {
Queue<String> taskQueue = new ArrayDeque<>();
taskQueue.offer("send-email");
taskQueue.offer("refresh-cache");
taskQueue.offer("push-message");
String headTask = taskQueue.peek();
String executedTask = taskQueue.poll();
System.out.println("Queue => " + taskQueue);
System.out.println("headTask => " + headTask);
System.out.println("executedTask => " + executedTask);
}
/**
* 演示 ArrayDeque 作为双端队列的用法。
*/
private static void useArrayDequeAsDeque() {
Deque<String> auditSteps = new ArrayDeque<>();
auditSteps.addFirst("load-user");
auditSteps.addLast("check-permission");
auditSteps.addLast("write-audit-log");
String firstStep = auditSteps.removeFirst();
String lastStep = auditSteps.peekLast();
System.out.println("Deque => " + auditSteps);
System.out.println("removedFirstStep => " + firstStep);
System.out.println("lastStep => " + lastStep);
}
/**
* 演示 PriorityQueue 的用法。
* 这里用更小的数字表示更高的优先级。
*/
private static void usePriorityQueue() {
PriorityQueue<Integer> priorities = new PriorityQueue<>();
priorities.offer(30);
priorities.offer(10);
priorities.offer(20);
Integer highestPriority = priorities.peek();
Integer executedPriority = priorities.poll();
System.out.println("PriorityQueue => " + priorities);
System.out.println("highestPriority => " + highestPriority);
System.out.println("executedPriority => " + executedPriority);
}
}5. Map:键值对存储
Map 是业务中使用频率最高的集合之一。
常见实现类包括:
HashMapLinkedHashMapTreeMapHashtableConcurrentHashMapEnumMap
5.1 HashMap
HashMap 是最容易被反复展开的主题之一。
特点
- 允许一个
null key - 允许多个
null value - 查询和插入平均性能较好
- 非线程安全
底层实现
JDK 8 以后,通常可以概括成:数组 + 链表 + 红黑树
大致过程是:
- 先根据
key的哈希值定位桶位置 - 如果桶里没有冲突,直接放入
- 如果有冲突,先挂链表
- 冲突严重时,链表可能树化成红黑树
你至少要记住两个核心点:
- 哈希表负责快速定位
- 树化是为了在冲突严重时优化性能
如果继续往下看,HashMap 最核心的底层状态通常包括:
table:桶数组size:当前键值对数量threshold:触发扩容的阈值loadFactor:负载因子
压缩后的核心结构可以理解成这样:
java
/**
* HashMap 的核心状态示意。
*/
public class HashMap<K, V> {
/**
* 哈希桶数组。
* 每个桶位上可能挂链表或红黑树。
*/
transient Node<K, V>[] table;
/**
* 当前实际元素个数。
*/
transient int size;
/**
* 触发扩容的阈值。
*/
int threshold;
/**
* 负载因子,默认常见值是 0.75。
*/
final float loadFactor;
}插入元素时,核心流程通常可以压缩成下面几步:
- 先算
key的哈希值 - 再定位桶位
- 桶为空就直接放
- 桶不为空就处理冲突
- 冲突链过长时可能树化
- 元素总量达到阈值时触发扩容
压缩后的 putVal 核心流程可以理解成这样:
java
/**
* HashMap.put 的核心流程示意。
*/
final V putVal(int hash, K key, V value) {
if (table == null || table.length == 0) {
resize();
}
int index = (table.length - 1) & hash;
Node<K, V> first = table[index];
if (first == null) {
table[index] = newNode(hash, key, value, null);
} else {
// 桶里已经有元素:可能走链表追加、key 覆盖、树节点插入等逻辑
putIntoCollisionBin(first, hash, key, value);
}
if (++size > threshold) {
resize();
}
return null;
}这里最值得建立的直觉是:
HashMap快,不是因为它“魔法快”- 而是因为它通过哈希把查找范围先缩小到某个桶
- 真正拖慢它的通常是哈希冲突和扩容
扩容时最关键的成本在于:
- 新建更大的桶数组
- 重新分布旧节点
- 这也是为什么大量写入场景下,初始容量设置不合理会带来明显额外成本
使用场景
适合:
- 按 key 快速查值
- 计数
- 缓存映射
- 各类业务状态映射
注意事项
key最好是不可变对象- 自定义
key时要正确重写hashCode()和equals() - 多线程下不能直接安全使用
- 不要把它的遍历顺序当成稳定顺序
5.2 LinkedHashMap
特点
- 保留插入顺序
- 也可以按访问顺序维护
- 查找性能仍然保留哈希结构的优势
底层实现
它是在 HashMap 基础上增加了双向链表来维护顺序。
如果从结构上理解,LinkedHashMap 可以看成:HashMap + 双向链表。
也就是说,它不是推翻了哈希结构,而是在每个节点上额外保存顺序指针。
压缩后的节点结构可以理解成这样:
java
/**
* LinkedHashMap 节点结构示意。
*/
static class Entry<K, V> extends HashMap.Node<K, V> {
/**
* 顺序链上的前驱节点。
*/
Entry<K, V> before;
/**
* 顺序链上的后继节点。
*/
Entry<K, V> after;
}这也是为什么它能做到两件事同时成立:
- 查找仍然主要依赖哈希定位
- 遍历顺序则由双向链表来保证
如果开启 accessOrder = true,那么每次访问节点后,核心思路还会把该节点移动到链表尾部。
这也是为什么它很适合做简易 LRU:最近访问的节点会不断往后移动,最久没访问的节点会留在头部。
使用场景
适合:
- 既要按 key 快速查找
- 又要稳定遍历顺序
- 简单 LRU 缓存实现
经典点在于它可以配合 removeEldestEntry 做简易 LRU。
5.3 TreeMap
特点
- 按 key 有序
- 支持自然排序和比较器排序
- 支持范围查询
底层实现
底层通常是红黑树。
如果再往下一层看,TreeMap 的底层重点不是“Map”,而是:它用红黑树维护 key 的有序性。
压缩后的节点结构可以理解成:
java
/**
* TreeMap 节点结构示意。
*/
static final class Entry<K, V> {
K key;
V value;
Entry<K, V> left;
Entry<K, V> right;
Entry<K, V> parent;
/**
* 红黑树节点颜色。
*/
boolean color;
}插入时最核心的流程通常是:
- 从根节点开始比较
key - 小于走左子树,大于走右子树
- 找到空位插入新节点
- 再执行红黑树平衡调整
所以它能支持:
- 自动排序
- 范围查询
- 向上取整、向下取整、最值查询
但代价也很明显:
- 每次插入和删除都不是简单数组操作
- 还要维护树的平衡
- 所以普通点查性能通常不如
HashMap
使用场景
适合:
- 需要按 key 排序
- 需要区间查询
- 需要最小 key、最大 key、向上取整、向下取整这类能力
注意事项
key必须可比较,或显式提供比较器- 性能通常不如
HashMap - 比较规则要稳定且符合业务语义
5.4 Hashtable
这是比较老的线程安全 Map 实现。
特点
- 方法大多直接同步
- 不允许
null key - 不允许
null value
底层实现
Hashtable 的底层大方向和早期哈希表实现类似,也是:数组 + 链式冲突处理。
它跟 HashMap 最大的区别不在“是不是哈希表”,而在:很多公开方法直接带 synchronized。
压缩后的核心写法可以看成这样:
java
/**
* Hashtable put 的核心思路示意。
*/
public synchronized V put(K key, V value) {
if (key == null || value == null) {
throw new NullPointerException();
}
return putIntoTable(key, value);
}这也是为什么:
- 它线程安全
- 但同步粒度比较粗
- 高并发下吞吐通常不如
ConcurrentHashMap
使用场景
现在更多是历史知识,一般不作为新代码首选。
5.5 ConcurrentHashMap
这是并发场景里最重要的 Map 实现之一。
特点
- 线程安全
- 并发性能明显好于粗粒度整表锁
- 不允许
null key - 不允许
null value
底层实现
JDK 8 中常见的理解是:
- 仍然有哈希桶结构
- 结合
CAS - 结合更细粒度的同步控制
所以它不是像 Hashtable 那样简单地把整张表全部锁住。
如果继续往下一层拆,ConcurrentHashMap 的核心状态通常会包含:
table:桶数组sizeCtl:初始化和扩容控制字段- 桶位上的节点链 / 树
最值得建立的判断是:它不是整表一把大锁,而是尽量让无冲突路径更轻,冲突时再进入局部同步。
压缩后的核心状态可以理解成这样:
java
/**
* ConcurrentHashMap 的核心状态示意。
*/
public class ConcurrentHashMap<K, V> {
/**
* 哈希桶数组。
* 使用 volatile 是为了让数组引用和桶位变更对其他线程可见。
*/
transient volatile Node<K, V>[] table;
/**
* 控制初始化、扩容阈值等行为的状态字段。
*/
private transient volatile int sizeCtl;
}插入时的核心思路通常可以压缩成下面几步:
- 先算哈希并定位桶位
- 桶为空时优先尝试
CAS放入 - 桶不为空时,对该桶进入同步流程
- 链过长时可能树化
压缩后的 putVal 核心流程可以理解成这样:
java
/**
* ConcurrentHashMap.put 的核心流程示意。
*/
final V putVal(K key, V value) {
for (;;) {
Node<K, V>[] tab = table;
int index = spread(key.hashCode()) & (tab.length - 1);
Node<K, V> first = tabAt(tab, index);
if (first == null) {
if (casTabAt(tab, index, null, new Node<>(key, value))) {
break;
}
} else {
synchronized (first) {
putIntoBin(first, key, value);
}
break;
}
}
return null;
}这里最值得读出来的是:
- 无冲突时尽量走
CAS - 冲突时只锁当前桶,不锁整张表
- 这也是为什么它的并发性能通常明显好于
Hashtable
它为什么线程安全,也可以压缩成三点:
- 写入时用
CAS和局部同步控制并发修改 - 关键状态用
volatile保证可见性 - 复合操作需要优先使用
putIfAbsent、computeIfAbsent这类原子方法
使用场景
适合:
- 多线程共享缓存
- 并发计数和状态映射
- 高并发读写场景下的键值存储
注意事项
- 不能存
null - 线程安全不代表复合操作天然原子
- 像“先判断再更新”这类逻辑,仍要考虑并发一致性
比如:
java
map.putIfAbsent(key, value);
map.computeIfAbsent(key, k -> load(k));这类原子方法往往比你手动 get 再 put 更稳妥。
5.6 EnumMap
这是一个很容易被忽略,但非常实用的实现类。
特点
key必须是枚举类型- 性能和空间利用率通常都很好
- 语义非常清晰
底层实现
因为枚举值集合固定,所以它可以基于更紧凑的结构实现,通常比通用 HashMap 更高效。
如果从数据结构上看,EnumMap 最值得记住的是:它并不需要像 HashMap 那样去做哈希定位。
因为枚举值集合是固定的,所以它通常会:
- 先拿到枚举常量数组
- 再通过
ordinal()直接定位槽位
压缩后的核心结构可以理解成这样:
java
/**
* EnumMap 的核心状态示意。
*/
public class EnumMap<K extends Enum<K>, V> {
/**
* 当前枚举类型的所有常量。
*/
private transient K[] keyUniverse;
/**
* 按枚举 ordinal 存放 value 的数组。
*/
private transient Object[] vals;
}这也是为什么它通常:
- 结构更紧凑
- 访问路径更直接
- 在“固定枚举 -> 值”这类场景下很自然
使用场景
适合:
- 用枚举作为状态键
- 固定类别映射
例如:
java
EnumMap<OrderStatus, String> descMap = new EnumMap<>(OrderStatus.class);5.7 一组最常见的 Map 用法示例
如果你想把最常见的 Map 操作一次串起来,可以重点记这些 API:
putgetcontainsKeyremove- 并发场景下的
putIfAbsent、computeIfAbsent
java
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
/**
* 演示几种常见 Map 实现的基本增删查用法。
*/
public class MapUsageDemo {
/**
* 订单状态枚举,用来演示 EnumMap。
*/
private enum OrderStatus {
CREATED,
PAYING,
FINISHED
}
public static void main(String[] args) {
useHashMap();
useLinkedHashMap();
useTreeMap();
useHashtable();
useConcurrentHashMap();
useEnumMap();
}
/**
* 演示 HashMap 的典型用法:
* 新增键值、查询值、判断 key 是否存在、删除键值对。
*/
private static void useHashMap() {
Map<Long, String> userNameMap = new HashMap<>();
userNameMap.put(1001L, "alice");
userNameMap.put(1002L, "bob");
String userName = userNameMap.get(1001L);
boolean containsBob = userNameMap.containsKey(1002L);
userNameMap.remove(1002L);
System.out.println("HashMap => " + userNameMap);
System.out.println("userName => " + userName);
System.out.println("containsBob => " + containsBob);
}
/**
* 演示 LinkedHashMap 的典型用法:
* 在普通 Map 操作基础上保留插入顺序。
*/
private static void useLinkedHashMap() {
Map<String, Integer> orderedScores = new LinkedHashMap<>();
orderedScores.put("java", 95);
orderedScores.put("mysql", 88);
orderedScores.put("redis", 92);
orderedScores.remove("mysql");
System.out.println("LinkedHashMap => " + orderedScores);
}
/**
* 演示 TreeMap 的典型用法:
* 自动按 key 排序,并支持范围查询。
*/
private static void useTreeMap() {
TreeMap<Integer, String> versionMap = new TreeMap<>();
versionMap.put(3, "v3");
versionMap.put(1, "v1");
versionMap.put(2, "v2");
String firstVersion = versionMap.firstEntry().getValue();
Map<Integer, String> headVersions = versionMap.headMap(3);
System.out.println("TreeMap => " + versionMap);
System.out.println("firstVersion => " + firstVersion);
System.out.println("headVersions => " + headVersions);
}
/**
* 演示 Hashtable 的基本用法。
* 这里只是帮助理解历史同步 Map API。
*/
private static void useHashtable() {
Hashtable<String, String> legacySettings = new Hashtable<>();
legacySettings.put("timeout", "30s");
legacySettings.put("retry", "3");
String timeout = legacySettings.get("timeout");
legacySettings.remove("retry");
System.out.println("Hashtable => " + legacySettings);
System.out.println("timeout => " + timeout);
}
/**
* 演示 ConcurrentHashMap 的典型用法:
* 使用更适合并发场景的原子复合方法。
*/
private static void useConcurrentHashMap() {
ConcurrentHashMap<String, Integer> routeCounter = new ConcurrentHashMap<>();
routeCounter.putIfAbsent("/orders", 0);
routeCounter.compute("/orders", (path, count) -> count == null ? 1 : count + 1);
routeCounter.computeIfAbsent("/users", path -> 1);
System.out.println("ConcurrentHashMap => " + routeCounter);
}
/**
* 演示 EnumMap 的典型用法:
* 用固定枚举类别作为 key 保存状态说明。
*/
private static void useEnumMap() {
EnumMap<OrderStatus, String> statusDescMap = new EnumMap<>(OrderStatus.class);
statusDescMap.put(OrderStatus.CREATED, "订单已创建");
statusDescMap.put(OrderStatus.PAYING, "订单支付中");
statusDescMap.put(OrderStatus.FINISHED, "订单已完成");
String finishedDesc = statusDescMap.get(OrderStatus.FINISHED);
statusDescMap.remove(OrderStatus.PAYING);
System.out.println("EnumMap => " + statusDescMap);
System.out.println("finishedDesc => " + finishedDesc);
}
}6. 并发集合怎么理解
Java 不只是有普通集合,还有专门面向并发场景的集合。
最常见的代表包括:
ConcurrentHashMapCopyOnWriteArrayListCopyOnWriteArraySetConcurrentSkipListSetBlockingQueue
6.1 BlockingQueue
它最典型的能力不是“存元素”,而是:在队列空或满时,支持阻塞式生产消费。
常见实现包括:
ArrayBlockingQueueLinkedBlockingQueuePriorityBlockingQueue
底层实现
BlockingQueue 是接口,不是某一个固定实现。
如果想先建立最核心的直觉,最适合抓 ArrayBlockingQueue 这类典型实现来看。
它的底层重点通常包括:
- 一个真正存元素的数组
- 一把保护入队和出队的锁
- 两个条件队列:
notEmpty和notFull
压缩后的核心结构可以理解成这样:
java
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* ArrayBlockingQueue 的核心状态示意。
*/
public class ArrayBlockingQueue<E> {
/**
* 真正存元素的有界数组。
*/
final Object[] items;
/**
* 保护队列状态的一把独占锁。
*/
final ReentrantLock lock = new ReentrantLock();
/**
* 队列为空时,消费者在这个条件上等待。
*/
private final Condition notEmpty = lock.newCondition();
/**
* 队列已满时,生产者在这个条件上等待。
*/
private final Condition notFull = lock.newCondition();
}例如 put 的核心流程,压缩后通常可以理解成:
java
/**
* BlockingQueue.put 的核心流程示意。
*/
public void put(E e) throws InterruptedException {
lock.lockInterruptibly();
try {
while (count == items.length) {
notFull.await();
}
enqueue(e);
notEmpty.signal();
} finally {
lock.unlock();
}
}而 take 的核心流程通常刚好相反:
java
/**
* BlockingQueue.take 的核心流程示意。
*/
public E take() throws InterruptedException {
lock.lockInterruptibly();
try {
while (count == 0) {
notEmpty.await();
}
E item = dequeue();
notFull.signal();
return item;
} finally {
lock.unlock();
}
}take 的核心思路则刚好相反:
- 队列空时等待
notEmpty - 取出元素后唤醒等待放入的线程
这也是为什么 BlockingQueue 适合生产者消费者模型:
- 共享队列本身负责缓存任务
- 队列满和队列空时的等待逻辑已经被内建进去
- 业务代码通常不需要自己手写
wait/notify
使用场景
适合:
- 生产者消费者模型
- 线程池任务队列
- 异步任务处理
注意事项
- 容量策略会影响系统背压
- 无界队列不是绝对安全,可能带来内存压力
6.2 BlockingQueue 用法示例
BlockingQueue 最值得先掌握的不是底层实现,而是几组常见操作:
put:队列满时会等待take:队列空时会等待offer:尝试放入poll:尝试取出
下面这个示例只保留主线,帮助你看懂“放任务”和“取任务”的基本写法。
java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/**
* 演示 BlockingQueue 的基本生产和消费写法。
*/
public class BlockingQueueUsageDemo {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> jobQueue = new ArrayBlockingQueue<>(3);
produce(jobQueue);
consume(jobQueue);
}
/**
* 模拟生产者向队列里放入任务。
*
* @param jobQueue 任务队列
* @throws InterruptedException 放入过程中如果线程被中断则抛出
*/
private static void produce(BlockingQueue<String> jobQueue) throws InterruptedException {
jobQueue.put("generate-report");
jobQueue.put("refresh-search-index");
jobQueue.offer("send-summary-email");
System.out.println("after produce => " + jobQueue);
}
/**
* 模拟消费者从队列里获取任务。
*
* @param jobQueue 任务队列
* @throws InterruptedException 获取过程中如果线程被中断则抛出
*/
private static void consume(BlockingQueue<String> jobQueue) throws InterruptedException {
String firstJob = jobQueue.take();
String secondJob = jobQueue.poll();
System.out.println("firstJob => " + firstJob);
System.out.println("secondJob => " + secondJob);
System.out.println("after consume => " + jobQueue);
}
}7. 怎么选:按需求反推集合类型
如果你不想死背类名,最稳妥的方式是按需求反推。
7.1 如果你需要“有序、可重复、按下标访问”
优先想:
ArrayList
7.2 如果你需要“唯一性”
优先想:
HashSet- 如果还要保留插入顺序,用
LinkedHashSet - 如果还要排序,用
TreeSet
7.3 如果你需要“按 key 快速查值”
优先想:
HashMap- 如果还要顺序,用
LinkedHashMap - 如果还要排序,用
TreeMap - 如果是并发场景,用
ConcurrentHashMap
7.4 如果你需要“队列 / 栈 / 双端队列”
优先想:
ArrayDeque- 如果要优先级,用
PriorityQueue - 如果要阻塞消费,用
BlockingQueue
8. 高频注意事项
8.1 hashCode() 和 equals() 必须协同
凡是放进 HashMap、HashSet 这类哈希集合中的对象,都要特别注意:
- 如果重写了
equals(),通常也要重写hashCode() - 否则会出现逻辑相等但无法正确查找或去重的问题
8.2 不要随便修改作为 key 的对象状态
如果一个对象已经作为 HashMap 的 key 放进去,而你又修改了参与哈希计算的字段,就可能导致:
- 找不回来
- 删除失败
- 逻辑错乱
所以更稳妥的做法是:把 key 设计成不可变对象。
8.3 迭代时修改集合要小心 ConcurrentModificationException
很多普通集合是 fail-fast 的。
也就是说,遍历过程中如果结构被意外修改,可能直接抛异常。
例如:
java
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
Integer num = it.next();
if (num % 2 == 0) {
it.remove();
}
}这里应该用迭代器自己的 remove,而不是直接 list.remove(...)。
8.4 Arrays.asList() 不是普通可变 ArrayList
这是经典坑。
java
List<String> list = Arrays.asList("a", "b", "c");它返回的不是 java.util.ArrayList,而是一个固定长度的列表视图。
所以:
- 可以改元素
- 不能随便
add或remove
8.5 subList() 是视图,不是完全独立副本
对 subList() 的修改,往往会影响原列表,反过来也可能受原列表结构变化影响。
如果你要的是独立列表,通常应该显式复制一份。
8.6 null 支持情况不要想当然
不同集合对 null 的支持不一样:
ArrayList允许nullHashMap允许一个null keyConcurrentHashMap不允许nullArrayDeque不允许null
这块也很容易被忽略。
9. 一段更适合放进正文里的总结说明
如果把这个问题压缩成一段完整说明“Java 集合类型有哪些,各有什么特点”,可以这样回答:
Java 集合框架大体可以分成 Collection 和 Map 两条线。Collection 下面主要有 List、Set、Queue;List 强调有序和可重复,典型实现是 ArrayList 和 LinkedList;Set 强调唯一性,常见有 HashSet、LinkedHashSet、TreeSet;Queue 和 Deque 更强调队列、双端队列和优先级队列语义,常见有 ArrayDeque 和 PriorityQueue。Map 主要是键值映射,最常见的是 HashMap、LinkedHashMap、TreeMap 和 ConcurrentHashMap。底层实现上,ArrayList 是动态数组,LinkedList 是双向链表,HashMap 是哈希桶加链表 / 红黑树,TreeMap 和 TreeSet 通常基于红黑树。实际选型时,要根据是否需要顺序、唯一性、排序、并发安全、随机访问和读写比例来判断。
10. 最后给一个简单选型表
如果你想快速记忆,可以记这张表:
- 读多、按下标访问多:
ArrayList - 去重:
HashSet - 去重且保留插入顺序:
LinkedHashSet - 去重且排序:
TreeSet - 普通键值映射:
HashMap - 映射且保留顺序:
LinkedHashMap - 映射且按 key 排序:
TreeMap - 并发映射:
ConcurrentHashMap - 栈 / 双端队列:
ArrayDeque - 优先级队列:
PriorityQueue
真正理解集合,不是背谁底层是数组、谁底层是链表,而是:
看到业务需求后,能立刻反推出应该选哪一类集合,以及为什么。