Java集合-cnblog

Java 常用集合完整总结

整体分为两大根接口:

  1. Collection 单列集合:存单个对象
    • List:有序、可重复
      • ArrayList、Vector、LinkedList、CopyOnWriteArrayList
    • Set:不可重复
      • HashSet、LinkedHashSet、TreeSet、CopyOnWriteArraySet
  2. Map 双列集合:键值对 key-value,key 不可重复
    • HashMap、LinkedHashMap、TreeMap、Hashtable、ConcurrentHashMap

联想截图_20260720100600

关键词

关键词汇总,学完这个可以去这篇文章,根据关键词进行梳理联想。
关键词汇总

一、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)实现原理

底层存储结构:数组 + 单向链表 + 红黑树
  1. 底层数组 table
    存放 Node<K,V> 节点,数组长度称为容量,默认初始容量 16,且容量永远是 2 的幂(16、32、64…)。
    好处:计算下标 hash & (table.length - 1) 等价于取模,运算效率远高于 %。

  2. 负载因子 loadFactor = 0.75
    触发扩容阈值 = 容量 × 0.75。
    例:容量16,阈值12;元素数量达到12,自动扩容。
    0.75是空间与哈希冲突的平衡值:太小浪费数组空间,太大链表过长查询变慢。

  3. 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

  4. 哈希冲突处理
    多个key算出相同数组下标,形成哈希桶:

    • 桶内节点数量 < 8:用单向链表串联;
    • 桶内节点数量 ≥ 8:链表转为红黑树(TreeNode),查询从O(n)优化为O(logn);
    • 扩容后桶内节点 ≤ 6:红黑树退化成链表(树维护成本高,节点少不需要树)。
  5. 扩容机制 resize()
    容量变为原来2倍;
    旧桶内节点只分两种位置:原下标 / 原下标+旧容量,不用重新计算hash,直接拆分迁移;

    说明:

    如果原来容量为16,那么len-1为1111,原来下标为key&01111
    现在容量为32,len-1为11111,讨论key在第4位是否为1
    如果为1,那么newIndex=oldIndex+oldCapicity
    如果为0,那么newIndex=oldIndex
    

    JDK8 使用尾插法,链表顺序不变,解决JDK7头插法并发扩容环形链表、CPU100%死循环问题。

  6. 辅助变量

    • size:当前实际键值对数量;
    • modCount:修改计数器,add/remove/clear都会自增,用于迭代器快速失败fail-fast。

2)优缺点

优点
  1. 读写性能极高,理想无冲突情况下时间复杂度 O(1);
  2. 允许 key=null、value=null,key最多只能存在一个null;
  3. 扩容、树化机制优化了哈希冲突场景的查询速度;
  4. API简单,日常业务存储键值对开销小。
缺点
  1. 线程完全不安全,无任何同步、CAS保护;
    • JDK7:多线程并发扩容,头插法倒置链表产生环形链表,get时死循环CPU占满;
    • JDK8:尾插消除死循环,但并发put仍会发生数据覆盖、size统计偏小、脏读;
  2. 有序性无法保证,元素存取顺序随机;
  3. 哈希冲突严重时,退化成链表,查询性能下降;
  4. 遍历过程中其他线程修改集合,会抛出 ConcurrentModificationException

3)线程安全相关

本身无锁、无volatile、无CAS,多线程共享会出现数据丢失、脏数据、计数错误。
两种线程安全替代方案:

  1. Collections.synchronizedMap(new HashMap<>())
    使用 SynchronizedMap 包装,所有方法加 synchronized 锁整个map,读写串行,并发性能差;遍历需要手动加锁,否则并发修改抛异常。
  2. ConcurrentHashMap(高并发推荐)
    JDK8 使用 CAS + 桶级synchronized,锁粒度细化到单个哈希桶,支持多线程并发读写,性能远优于同步Map,不允许key/value为null。

4)适用场景

  1. 单线程环境下普通键值映射存储;
  2. 本地临时缓存、参数映射、数据分组;
  3. 不需要多线程并发操作、追求读写速度的业务;
  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核心改动)
  1. 空桶插入使用 CAS 无锁操作
    当目标哈希桶 table[i] 为 null 时,调用 casTabAt 基于CPU硬件CAS指令,尝试直接把新节点放入数组下标;
    CAS成功直接写入,全程不加锁,无内核切换开销;
    CAS失败代表该桶被其他线程抢先写入,进入加锁分支。

  2. 桶有数据时,synchronized 锁住当前桶头节点
    不再像JDK7那样使用Segment分段全局锁,锁粒度缩小到单个哈希桶的头节点
    只锁住当前操作的桶,其他哈希桶完全并发读写,极大提升并发吞吐量;
    锁范围仅覆盖当前桶的链表/红黑树修改逻辑,不会阻塞其他桶。

多线程协助扩容(transfer 机制)
  1. 触发扩容条件和HashMap一致:元素数量达到 容量×0.75
  2. 扩容容量翻倍,新建两倍长度的空数组 nextTable
  3. 单线程扩容会阻塞并发,JDK8优化:多线程可以一起协助迁移数据
  4. 迁移时会给当前处理的桶打上扩容标记,其他线程发现正在扩容,会主动帮忙搬运旧桶数据;
  5. 读写线程遇到正在迁移的桶,会先协助扩容完成,再执行读写操作;
  6. 扩容全程不会阻塞其他桶的并发读写,解决了旧版本扩容全局阻塞的问题。

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)优缺点

优点
  1. 高并发读写性能极强
    空桶CAS无锁写入,有数据仅锁单个桶,多线程可同时操作不同哈希桶,并发度远高于 Collections.synchronizedMap
  2. 锁粒度极小,无全局锁
    synchronized只锁当前桶头,不存在锁整张表的情况,线程竞争冲突大幅减少;
  3. 并发扩容支持多线程协助迁移
    扩容不会阻塞全表读写,高并发大量写入场景性能下滑不严重;
  4. 完美规避HashMap并发问题:无数据覆盖、无死循环、无size计数错乱。
缺点
  1. 底层源码逻辑复杂
    融合CAS、synchronized、扩容迁移、树化、多线程协助搬运,维护难度远高于HashMap;
  2. 不支持key/value存null
    需要业务手动做空值包装处理;
  3. 批量操作弱一致性
    size()containsValue()、遍历等批量操作不加锁,执行过程中数据可能被其他线程修改,拿到的是近似值,无法保证实时精确;
  4. 遍历无快速失败,并发修改不会报错,需要业务自行处理数据一致性问题。

3)线程安全实现

整体依靠两套机制配合保证并发安全:

  1. CAS无锁操作
    写入空哈希桶时,使用Unsafe底层CAS原子指令,保证多个线程竞争空桶时只有一个线程写入成功,避免数据覆盖;
  2. 桶级synchronized独占锁
    哈希桶已有节点时,锁住该桶第一个Node对象;同一时间仅一个线程能修改该桶链表/红黑树,防止链表结构错乱;
  3. 扩容安全控制
    通过扩容标记、迁移步长控制多线程协作搬运,避免多线程重复迁移、丢失数据;
  4. 数组table使用volatile修饰
    保证扩容后新数组引用对所有线程立即可见,读取时不会一直读取旧数组。

补充对比:

  • Collections.synchronizedMap:全局锁,同一时间只能一个线程读写整个Map;
  • ConcurrentHashMap:桶级锁,多线程可同时操作不同桶,并发能力差距巨大。

4)适用场景

  1. 多线程并发环境下的键值存储,服务端全局共享映射;
  2. 高并发本地内存缓存,替代加锁的HashMap;
  3. 接口并发请求、多线程任务共享中间数据;
  4. 并发计数器、分布式本地临时映射表;
  5. 不适合:需要强一致性批量统计、需要存储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 控制)
  1. accessOrder = false(默认:插入顺序)
    每次 put 新增元素,直接挂载到双向链表尾部;调用 get() 查询元素不会改变链表顺序
    遍历 Map 时,输出顺序和你 put 的先后顺序完全一致。

  2. 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

  1. 存入顺序:A → B → C,链表顺序 head:A → B → C:tail
  2. 访问A,accessOrder=true,A移到尾部:head:B → C → A:tail
  3. 新增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;
    }
};

三、优缺点

优点
  1. 基于 HashMap,读写依旧接近 O(1),性能很高;
  2. 天然支持两种有序:插入有序 / 访问有序;
  3. 内置双向链表+淘汰钩子,无需自己维护时间戳、访问时间,快速实现轻量本地LRU缓存;
  4. 迭代遍历顺序可控,HashMap 遍历完全无序。
缺点
  1. 每个节点多 before/after 两个指针,额外占用内存,内存开销比 HashMap 更大;
  2. 每次访问元素(LRU模式)需要修改双向链表指针,有轻微性能损耗;
  3. 底层依旧是 HashMap,线程不安全,多线程并发读写会出现数据丢失、并发修改异常。

四、线程安全

本身完全线程不安全,底层HashMap无同步机制。
并发场景方案:

  1. Collections.synchronizedMap(new LinkedHashMap<>()) 全局加锁,性能差;
  2. 高并发缓存场景不推荐,一般用 Redis / Caffeine 本地缓存替代。

五、适用场景

  1. 需要保证存取有序的键值对存储(如接口参数有序返回);
  2. 单线程下小型本地内存缓存,需要自动淘汰旧数据(简易LRU);
  3. 遍历时需要按插入顺序展示数据。

补充区分

  • HashMap:无序,不能做LRU;
  • LinkedHashMap:可插入有序,可开启访问有序实现LRU;
  • TreeMap:按键排序,不记录访问时间,无法实现LRU。
posted @ 2026-07-20 12:01  alij  阅读(5)  评论(0)    收藏  举报