Java集合-cnblog
Java 常用集合完整总结
整体分为两大根接口:
- Collection 单列集合:存单个对象
- List:有序、可重复
- ArrayList、Vector、LinkedList、CopyOnWriteArrayList
- Set:不可重复
- HashSet、LinkedHashSet、TreeSet、CopyOnWriteArraySet
- List:有序、可重复
- Map 双列集合:键值对 key-value,key 不可重复
- HashMap、LinkedHashMap、TreeMap、Hashtable、ConcurrentHashMap

关键词
关键词汇总,学完这个可以去这篇文章,根据关键词进行梳理联想。
关键词汇总
一、List 系列(有序、可重复、支持下标)
1. ArrayList
1)实现原理
底层 Object[] 动态数组;无参构造默认空数组,首次add扩容至10,后续每次扩容1.5倍,如果经过一次扩容后仍然小于期望容量,那么直接使用期望容量;随机访问直接数组下标;中间增删通过 System.arraycopy 移动元素。
2)优缺点
- 优点:随机查询
get()O(1),遍历速度快,内存连续开销小 - 缺点:中间/头部增删需要数组拷贝,性能 O(n);扩容会产生多余闲置数组空间
3)线程安全
不安全,多线程add会数组越界、数据丢失、并发遍历抛 ConcurrentModificationException
安全方案:
Collections.synchronizedList(new ArrayList<>()):方法加synchronized锁整个集合- 读多写少:
CopyOnWriteArrayList
4)适用场景
查询多、增删少、单线程环境;日常存储遍历数据。
2. LinkedList
1)实现原理
双向链表,内部静态内部类 Node 存储元素、前驱、后继指针;无容量限制,无需扩容;首尾增删只修改指针,中间查询需要遍历链表。
2)优缺点
- 优点:首尾插入删除 O(1),无需数组拷贝;无扩容浪费空间
- 缺点:随机查询必须遍历,
get(index)O(n);内存碎片化,每个节点额外存指针
3)线程安全
不安全
安全方案:Collections.synchronizedList(new LinkedList<>())
4)适用场景
频繁首尾增删、少量查询;用作队列、栈。
3. Vector(过时)
1)实现原理
和ArrayList几乎一致,底层Object数组,扩容2倍。
2)优缺点
优点:自带同步;缺点:全方法synchronized锁,并发性能极差;扩容倍数更大,空间浪费多
3)线程安全
安全,所有方法加 synchronized
4)适用场景
几乎淘汰,不推荐使用。
4. CopyOnWriteArrayList
1)实现原理
写时复制:底层数组,修改(add/remove/set)时加锁,拷贝一份新数组操作,完成后替换原数组;读操作不加锁,直接访问旧数组。
2)优缺点
- 优点:读无锁,高并发读性能好;遍历不会抛快速失败异常
- 缺点:写操作复制数组,内存开销大、写入慢;读取到旧数据(弱一致性)
3)线程安全
线程安全;写加 ReentrantLock,读无锁
4)适用场景
读多写少场景:缓存、配置列表。
二、Set 系列(元素不可重复,基于Map实现)
1. HashSet
1)实现原理
底层封装 HashMap,元素作为 Map 的 key,value 统一为静态常量 Object中的PRESENT;依靠 hashCode()+equals() 去重;无序。
2)优缺点
- 优点:添加、查询、去重效率接近 O(1)
- 缺点:由于底层实现是HashMap,JDK7采用的是头插法,并发扩容死循环,JDK8数据丢失;
3)线程安全
不安全
安全方案:Collections.synchronizedSet(new HashSet<>())、CopyOnWriteArraySet
4)适用场景
去重存储,不关心顺序,单线程。
2. LinkedHashSet
1)实现原理
底层 LinkedHashMap,在HashMap基础上增加双向链表,记录插入顺序。
2)优缺点
优点:有序(插入顺序);查询效率和HashSet接近
缺点:维护链表指针,内存开销略高于HashSet
3)线程安全
不安全
4)适用场景:需要去重且保留插入顺序。
3. TreeSet
1)实现原理
底层 TreeMap,红黑树;元素必须实现 Comparable 或传入比较器,自动升序排序。
2)优缺点
优点:元素自动排序;范围查询(大于、小于)友好
缺点:增删查 O(logn),比HashSet慢;元素必须可比较,不能存null
3)线程安全
不安全
4)适用场景:需要自动排序的去重集合。
4. CopyOnWriteArraySet
1)实现原理
底层封装 CopyOnWriteArrayList,通过addIfAbsent去重(时间复杂度\(O(n)\))。
2)优缺点
同CopyOnWriteArrayList:读快写慢,内存消耗大
3)线程安全:安全
4)适用场景:读多写少、需要线程安全的去重集合。
三、Map 系列(键值对,key唯一)
1. HashMap(高频)
1)实现原理
底层存储结构:数组 + 单向链表 + 红黑树
-
底层数组 table
存放Node<K,V>节点,数组长度称为容量,默认初始容量 16,且容量永远是 2 的幂(16、32、64…)。
好处:计算下标hash & (table.length - 1)等价于取模,运算效率远高于 %。 -
负载因子 loadFactor = 0.75
触发扩容阈值 = 容量 × 0.75。
例:容量16,阈值12;元素数量达到12,自动扩容。
0.75是空间与哈希冲突的平衡值:太小浪费数组空间,太大链表过长查询变慢。 -
hash 扰动函数
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }将 hashCode 高16位与低16位异或,让高低位特征都参与下标计算,减少哈希碰撞。
key 为 null 时 hash=0,固定放在数组下标0位置,所以只能存一个 null key。 -
哈希冲突处理
多个key算出相同数组下标,形成哈希桶:- 桶内节点数量 < 8:用单向链表串联;
- 桶内节点数量 ≥ 8:链表转为红黑树(TreeNode),查询从O(n)优化为O(logn);
- 扩容后桶内节点 ≤ 6:红黑树退化成链表(树维护成本高,节点少不需要树)。
-
扩容机制 resize()
容量变为原来2倍;
旧桶内节点只分两种位置:原下标 / 原下标+旧容量,不用重新计算hash,直接拆分迁移;说明:
如果原来容量为16,那么len-1为1111,原来下标为key&01111 现在容量为32,len-1为11111,讨论key在第4位是否为1 如果为1,那么newIndex=oldIndex+oldCapicity 如果为0,那么newIndex=oldIndexJDK8 使用尾插法,链表顺序不变,解决JDK7头插法并发扩容环形链表、CPU100%死循环问题。
-
辅助变量
- size:当前实际键值对数量;
- modCount:修改计数器,add/remove/clear都会自增,用于迭代器快速失败fail-fast。
2)优缺点
优点
- 读写性能极高,理想无冲突情况下时间复杂度 O(1);
- 允许 key=null、value=null,key最多只能存在一个null;
- 扩容、树化机制优化了哈希冲突场景的查询速度;
- API简单,日常业务存储键值对开销小。
缺点
- 线程完全不安全,无任何同步、CAS保护;
- JDK7:多线程并发扩容,头插法倒置链表产生环形链表,get时死循环CPU占满;
- JDK8:尾插消除死循环,但并发put仍会发生数据覆盖、size统计偏小、脏读;
- 有序性无法保证,元素存取顺序随机;
- 哈希冲突严重时,退化成链表,查询性能下降;
- 遍历过程中其他线程修改集合,会抛出
ConcurrentModificationException。
3)线程安全相关
本身无锁、无volatile、无CAS,多线程共享会出现数据丢失、脏数据、计数错误。
两种线程安全替代方案:
Collections.synchronizedMap(new HashMap<>())
使用SynchronizedMap包装,所有方法加 synchronized 锁整个map,读写串行,并发性能差;遍历需要手动加锁,否则并发修改抛异常。ConcurrentHashMap(高并发推荐)
JDK8 使用 CAS + 桶级synchronized,锁粒度细化到单个哈希桶,支持多线程并发读写,性能远优于同步Map,不允许key/value为null。
4)适用场景
- 单线程环境下普通键值映射存储;
- 本地临时缓存、参数映射、数据分组;
- 不需要多线程并发操作、追求读写速度的业务;
- 不适合多线程共享,并发场景必须替换为 ConcurrentHashMap。
2. LinkedHashMap
1)实现原理
继承HashMap,额外双向链表维护插入/访问顺序;可开启LRU淘汰策略(重写removeEldestEntry)。
2)优缺点
优点:有序;支持LRU本地缓存
缺点:维护链表,内存开销略大
3)线程安全:不安全
4)适用场景:有序键值对、简易本地LRU缓存。
3. TreeMap
1)实现原理
底层红黑树,key实现Comparable或自定义比较器,按键自动排序。
2)优缺点
优点:key有序,支持区间查询
缺点:读写O(logn),性能低于HashMap;key不能为null
3)线程安全:不安全
4)适用场景:需要按key排序的映射。
4. Hashtable(过时)
1)实现原理
数组+链表,扩容2倍;不允许key/value为null。
2)优缺点
优点:线程安全;缺点:全方法synchronized锁整张表,并发性能极差
3)线程安全:安全,方法加synchronized
4)适用场景:废弃,新项目禁用。
5. ConcurrentHashMap(并发首选)
1)实现原理
基础底层结构
底层依旧是 Node<K,V>[] table,结构为 数组 + 单向链表 + 红黑树,和 HashMap 存储结构一致;
默认初始容量16,负载因子固定0.75,容量必须是2的幂,哈希扰动函数逻辑和HashMap相同;
链表长度≥8树化,≤6退链表,哈希冲突处理逻辑一致。
核心并发设计:CAS + synchronized 桶级锁(JDK8核心改动)
-
空桶插入使用 CAS 无锁操作
当目标哈希桶table[i]为 null 时,调用casTabAt基于CPU硬件CAS指令,尝试直接把新节点放入数组下标;
CAS成功直接写入,全程不加锁,无内核切换开销;
CAS失败代表该桶被其他线程抢先写入,进入加锁分支。 -
桶有数据时,synchronized 锁住当前桶头节点
不再像JDK7那样使用Segment分段全局锁,锁粒度缩小到单个哈希桶的头节点;
只锁住当前操作的桶,其他哈希桶完全并发读写,极大提升并发吞吐量;
锁范围仅覆盖当前桶的链表/红黑树修改逻辑,不会阻塞其他桶。
多线程协助扩容(transfer 机制)
- 触发扩容条件和HashMap一致:元素数量达到
容量×0.75; - 扩容容量翻倍,新建两倍长度的空数组
nextTable; - 单线程扩容会阻塞并发,JDK8优化:多线程可以一起协助迁移数据;
- 迁移时会给当前处理的桶打上扩容标记,其他线程发现正在扩容,会主动帮忙搬运旧桶数据;
- 读写线程遇到正在迁移的桶,会先协助扩容完成,再执行读写操作;
- 扩容全程不会阻塞其他桶的并发读写,解决了旧版本扩容全局阻塞的问题。
ConcurrentHashMap的transfer是扩容迁移核心方法,依靠ForwardingNode标记识别扩容状态;单线程触发扩容创建新数组后,其他读写线程检测到扩容不会阻塞,会认领未处理桶并行协助搬运数据;迁移按区间分工,仅处理中的桶会临时转移,其余哈希桶可正常并发读写,大幅缩短扩容耗时,解决扩容全局阻塞问题,减少高并发写入时的性能抖动。
示例:
初始 table 长度 16,达到阈值触发扩容,新建 nextTable 长度 32:
线程 1 执行 put,发现需要扩容,创建 nextTable,认领桶 0~15 开始迁移;
线程 2 同时执行 put,遍历 table 发现存在 ForwardingNode,判定正在扩容,认领桶 16~31 协助搬运;
线程 3 查询桶 5(正在迁移中),暂停查询,帮忙搬运桶 5 数据;
线程 4 插入桶 20(未迁移区间),不受扩容影响,直接写入;
所有桶搬运完成后,table 指向 nextTable,扩容结束,删除扩容标记。
不允许 key、value 为 null
HashMap允许一个null key、多个null value,但ConcurrentHashMap直接禁止null:
并发场景下无法区分「key不存在」和「key存在值为null」,会出现歧义,因此put时传入null直接抛空指针。
无 modCount,弱一致性迭代
没有快速失败机制,遍历迭代器不校验修改计数;
遍历过程中其他线程新增、删除数据,迭代器可能读到新数据、也可能读不到,不会抛出ConcurrentModificationException,称为弱一致性。
2)优缺点
优点
- 高并发读写性能极强
空桶CAS无锁写入,有数据仅锁单个桶,多线程可同时操作不同哈希桶,并发度远高于Collections.synchronizedMap; - 锁粒度极小,无全局锁
synchronized只锁当前桶头,不存在锁整张表的情况,线程竞争冲突大幅减少; - 并发扩容支持多线程协助迁移
扩容不会阻塞全表读写,高并发大量写入场景性能下滑不严重; - 完美规避HashMap并发问题:无数据覆盖、无死循环、无size计数错乱。
缺点
- 底层源码逻辑复杂
融合CAS、synchronized、扩容迁移、树化、多线程协助搬运,维护难度远高于HashMap; - 不支持key/value存null
需要业务手动做空值包装处理; - 批量操作弱一致性
size()、containsValue()、遍历等批量操作不加锁,执行过程中数据可能被其他线程修改,拿到的是近似值,无法保证实时精确; - 遍历无快速失败,并发修改不会报错,需要业务自行处理数据一致性问题。
3)线程安全实现
整体依靠两套机制配合保证并发安全:
- CAS无锁操作
写入空哈希桶时,使用Unsafe底层CAS原子指令,保证多个线程竞争空桶时只有一个线程写入成功,避免数据覆盖; - 桶级synchronized独占锁
哈希桶已有节点时,锁住该桶第一个Node对象;同一时间仅一个线程能修改该桶链表/红黑树,防止链表结构错乱; - 扩容安全控制
通过扩容标记、迁移步长控制多线程协作搬运,避免多线程重复迁移、丢失数据; - 数组table使用volatile修饰
保证扩容后新数组引用对所有线程立即可见,读取时不会一直读取旧数组。
补充对比:
Collections.synchronizedMap:全局锁,同一时间只能一个线程读写整个Map;- ConcurrentHashMap:桶级锁,多线程可同时操作不同桶,并发能力差距巨大。
4)适用场景
- 多线程并发环境下的键值存储,服务端全局共享映射;
- 高并发本地内存缓存,替代加锁的HashMap;
- 接口并发请求、多线程任务共享中间数据;
- 并发计数器、分布式本地临时映射表;
- 不适合:需要强一致性批量统计、需要存储null键/值的场景。
快速汇总记忆表
List
| 集合 | 底层 | 线程安全 | 核心场景 |
|---|---|---|---|
| ArrayList | 动态数组 | 不安全 | 查询多、单线程 |
| LinkedList | 双向链表 | 不安全 | 首尾频繁增删 |
| Vector | 动态数组 | 安全 | 淘汰,不使用 |
| CopyOnWriteArrayList | 写时复制数组 | 安全 | 读多写少并发 |
Set
| 集合 | 底层 | 线程安全 | 特性 |
|---|---|---|---|
| HashSet | HashMap | 不安全 | 无序去重 |
| LinkedHashSet | LinkedHashMap | 不安全 | 插入有序去重 |
| TreeSet | TreeMap | 不安全 | 自动排序去重 |
| CopyOnWriteArraySet | CopyOnWriteList | 安全 | 并发读多写少去重 |
Map
| 集合 | 底层 | 线程安全 | 特性 |
|---|---|---|---|
| HashMap | 数组+链表+红黑树 | 不安全 | 高性能无序键值 |
| LinkedHashMap | HashMap+双向链表 | 不安全 | 有序、LRU缓存 |
| TreeMap | 红黑树 | 不安全 | key自动排序 |
| Hashtable | 数组+链表 | 安全 | 过时,性能差 |
| ConcurrentHashMap | CAS+synchronized桶锁 | 安全 | 高并发键值存储首选 |
学习补充点
快速失败机制
基本所有集合都继承了抽象类的modCount(ConcurrentHashMap除外,使用CAS+synchronized),用于记录操作集合的次数,当我们初始化一个迭代器的时候,会有下面操作:
expectedModCount = modCount
但是每次 add/remove/clear 都会 modCount++,遍历过程中如果外部修改集合,modCount 改变,迭代器 next () 时抛出:
ConcurrentModificationException
如果是迭代器自身删除元素,由于迭代器内部有expectedModCount,会和modCount进行同步更新,所以不会抛出异常。
只用于并发报错提示,不保证线程安全。
Collections.synchronizedList(new LinkedList<>())
Collections.synchronizedList() 会返回一个 SynchronizedList 包装对象,它是 Collections 的静态内部类,持有原 List(LinkedList/ArrayList)和一个锁对象 mutex。操作的仍然是自己创建的那个对象,只是进行了一个包装,相当于代理对象。
static class SynchronizedList<E> implements List<E> {
final List<E> list;
// 锁对象:默认是当前 SynchronizedList 实例 this
final Object mutex;
// 构造方法
SynchronizedList(List<E> list) {
this.list = list;
this.mutex = this; // 锁就是包装类自身
}
// 所有方法都加 synchronized(mutex)
public boolean add(E e) {
synchronized (mutex) {
return list.add(e);
}
}
public E get(int index) {
synchronized (mutex) {
return list.get(index);
}
}
public E remove(int index) {
synchronized (mutex) {
return list.remove(index);
}
}
public Iterator<E> iterator() {
synchronized (mutex) {
return list.iterator();
}
}
}
CopyOnWriteArrayList
这个是适合于读多写少的场景,写是加了锁的,读没有加锁。
数组使用volatile修饰,保证了多线程间数组的可加性;
如果不使用volatile,每个线程一般有自己的缓存,读到的就是旧数据,而使用volatile之后,每次都会从最新的地址去读数据。
核心思想就是写时复制:写时先复制一个副本,我们对副本就行操作,其他线程仍然可以读取到数据,当操作完副本后,立马使用volatile更新数组的引用,其他数组就可以读到新数据。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock(); // 1. 加锁
try {
Object[] oldArr = getArray();
int len = oldArr.length;
// 2. 拷贝新数组,长度+1
Object[] newArr = Arrays.copyOf(oldArr, len + 1);
newArr[len] = e;
// 3. 替换volatile数组引用
setArray(newArr);
return true;
} finally {
lock.unlock(); // 释放锁
}
}
优点:适合写少,读多的高并发场景;
缺点:弱一致性,会有短暂读取到旧数据;
LinkedHashMap详解
一、实现原理详细拆解
1. 继承关系与节点结构
LinkedHashMap<K,V> extends HashMap<K,V>
完全复用 HashMap 的底层:数组+链表+红黑树、hash扰动、扩容、树化规则全部不变。
只是重写了 HashMap 的 Node,扩展成双向链表节点:
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before; // 前驱节点
Entry<K,V> after; // 后继节点
Entry(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
全局维护 head(双向链表头部)、tail(双向链表尾部),所有存入 Map 的键值对都会串联在这条双向链表上,和哈希桶内的链表互不干扰。
2. 两种排序模式(由构造参数 accessOrder 控制)
-
accessOrder = false(默认:插入顺序)
每次put新增元素,直接挂载到双向链表尾部;调用get()查询元素不会改变链表顺序。
遍历 Map 时,输出顺序和你 put 的先后顺序完全一致。 -
accessOrder = true(访问顺序,LRU 模式)
调用get(key)/put(key,新值)访问已有元素时,会把当前节点从链表原有位置移除,移动到链表尾部。
链表头部永远是最久没有被访问的数据,尾部是最近访问的数据,这是实现 LRU 的核心基础。
3. LRU 淘汰钩子方法:removeEldestEntry
新增元素 put 完成后,源码会自动调用这个方法:
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
- 默认返回
false:不删除任何数据,无限存储; - 我们重写该方法,判断当前元素数量超过阈值时返回
true,框架会自动删除链表头部最久未使用的元素(eldest),实现缓存自动淘汰。
二、LRU 是什么(Least Recently Used,最近最少使用)
1. 定义
缓存淘汰算法:当缓存容量达到上限,优先淘汰最久没有被访问的数据,保留近期高频访问的数据。
2. LRU 简单举例
缓存最大存3条数据:A、B、C
- 存入顺序:A → B → C,链表顺序 head:A → B → C:tail
- 访问A,accessOrder=true,A移到尾部:head:B → C → A:tail
- 新增D,缓存满了,淘汰头部最久未访问的B,最终保留 C、A、D
3. LinkedHashMap 实现简易LRU代码示例
// 最大缓存10条,超过自动淘汰最久未使用
Map<String,Object> cache = new LinkedHashMap<>(16,0.75f,true){
@Override
protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
return size() > 10;
}
};
三、优缺点
优点
- 基于 HashMap,读写依旧接近 O(1),性能很高;
- 天然支持两种有序:插入有序 / 访问有序;
- 内置双向链表+淘汰钩子,无需自己维护时间戳、访问时间,快速实现轻量本地LRU缓存;
- 迭代遍历顺序可控,HashMap 遍历完全无序。
缺点
- 每个节点多
before/after两个指针,额外占用内存,内存开销比 HashMap 更大; - 每次访问元素(LRU模式)需要修改双向链表指针,有轻微性能损耗;
- 底层依旧是 HashMap,线程不安全,多线程并发读写会出现数据丢失、并发修改异常。
四、线程安全
本身完全线程不安全,底层HashMap无同步机制。
并发场景方案:
Collections.synchronizedMap(new LinkedHashMap<>())全局加锁,性能差;- 高并发缓存场景不推荐,一般用 Redis / Caffeine 本地缓存替代。
五、适用场景
- 需要保证存取有序的键值对存储(如接口参数有序返回);
- 单线程下小型本地内存缓存,需要自动淘汰旧数据(简易LRU);
- 遍历时需要按插入顺序展示数据。
补充区分
- HashMap:无序,不能做LRU;
- LinkedHashMap:可插入有序,可开启访问有序实现LRU;
- TreeMap:按键排序,不记录访问时间,无法实现LRU。

浙公网安备 33010602011771号