Skip to content

Java 集合详解

Java 集合既是业务代码里最常用的基础设施之一,也是理解 Java 数据组织方式时绕不开的一块。

很多人一开始会把集合记成几个类名:

  1. ArrayList
  2. LinkedList
  3. HashSet
  4. HashMap

只记住这些类名当然不够,继续往下看时,更重要的是把下面这些问题理顺:

  1. 它们分别属于什么集合类型
  2. 各自的特点是什么
  3. 底层是怎么实现的
  4. 适合什么使用场景
  5. 有哪些常见坑和注意事项

这篇文章就按这个顺序,把 Java 集合体系完整整理一遍。

1. 先建立整体认识:Collection 和 Map 不是一回事

Java 集合框架可以粗分成两大体系:

  1. Collection
  2. Map

它们的关系不是上下级包含,而是并列的两条线。

1.1 Collection

Collection 更偏“单值集合”,也就是一组元素的管理。

它下面最常见的三类是:

  1. List
  2. Set
  3. Queue / 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

这张树最重要的不是一次把所有类名都背下来,而是先建立两个认知:

  1. CollectionMap 是并列主线,不是包含关系
  2. List / Set / Queue 属于 Collection 体系,而 HashMap / TreeMap / ConcurrentHashMap 属于 Map 体系

如果只想先记最常用的一层,也可以压缩成:

text
Collection
├── List
├── Set
└── Queue

Map
├── HashMap
├── LinkedHashMap
├── TreeMap
└── ConcurrentHashMap

1.3 两张总表:把 CollectionMap 的常见实现看清楚

如果你不想一开始就陷入每个类的底层细节,最稳妥的方式是看两张总表。

先记一个大前提:

  1. Collection 体系的共同点是:都在管理“一组单值元素”
  2. Map 体系的共同点是:都在管理“key -> value 映射关系”

真正拉开差异的,通常是:

  1. 是否允许重复
  2. 是否保留顺序
  3. 是否支持排序
  4. 底层结构是什么
  5. 是否线程安全
  6. 更适合什么业务场景

1.3.1 Collection 常见实现对比表

分类实现类共同点不同点适用场景线程安全null 支持
ListArrayList都属于 Collection 体系,管理单值元素动态数组;有序、可重复;按下标访问快;中间插删慢读多写少、频繁遍历、按索引访问允许
ListLinkedList都属于 Collection 体系,管理单值元素双向链表;有序、可重复;头尾插删灵活;随机访问慢频繁头尾操作、同时想当 Deque允许
ListVector都属于 Collection 体系,管理单值元素动态数组;方法大多自带同步;历史实现色彩更重老代码兼容、了解历史线程安全列表是,但粒度较粗允许
ListCopyOnWriteArrayList都属于 Collection 体系,管理单值元素写时复制;读多写少友好;写入成本高;迭代读快照监听器、配置列表、白名单等读多写少场景允许
SetHashSet都属于 Collection 体系,强调单值元素唯一性基于哈希;无序;去重依赖 hashCode / equals去重、判断元素是否存在允许一个
SetLinkedHashSet都属于 Collection 体系,强调单值元素唯一性在哈希基础上维护插入顺序去重且要稳定输出顺序允许一个
SetTreeSet都属于 Collection 体系,强调单值元素唯一性基于红黑树;自动排序;支持范围查询排序去重、最值和区间场景一般不建议放,比较时容易出问题
SetCopyOnWriteArraySet都属于 Collection 体系,强调单值元素唯一性写时复制;读多写少友好;迭代读快照监听器集合、白名单、配置开关集合允许一个
SetConcurrentSkipListSet都属于 Collection 体系,强调单值元素唯一性基于跳表;线程安全;元素有序;支持范围查询并发有序去重、区间查询不允许
Queue / DequeArrayDeque都属于 Collection 体系,强调元素按特定语义取用循环数组;头尾操作高效;可作队列也可作栈普通队列、双端队列、栈替代 Stack不允许
QueuePriorityQueue都属于 Collection 体系,强调元素按特定语义取用基于堆;不是 FIFO;队头优先级最高Top K、调度、优先级任务不允许
并发队列BlockingQueue都属于 Collection 体系,强调元素按特定语义取用支持阻塞式生产消费;常见有界/无界实现生产者消费者、线程池任务队列是,具体看实现通常不允许

读这张表时,最值得先记住的是:

  1. 普通列表优先想 ArrayList
  2. 去重优先想 HashSet
  3. 映射关系不在这张表里,而在 Map 那张表里
  4. 队列和双端队列优先想 ArrayDeque
  5. 并发读多写少的列表才优先考虑 CopyOnWriteArrayList
  6. 线程安全且读多写少的 Set 可以优先考虑 CopyOnWriteArraySet
  7. 线程安全且还要有序的 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 的特殊场景无稳定业务顺序语义允许

读这张表时,最值得先记住的是:

  1. 普通映射优先想 HashMap
  2. 要顺序就想 LinkedHashMap
  3. 要排序就想 TreeMap
  4. 并发场景优先想 ConcurrentHashMap
  5. EnumMapWeakHashMapIdentityHashMap 都是偏特殊语义场景

1.4 CollectionCollections 的区别

这是很容易混淆的一道题。

  1. Collection 是集合顶层接口
  2. Collections 是工具类

Collections 里提供的是:

  1. 排序
  2. 查找
  3. 同步包装
  4. 不可变集合包装

所以不要把它们当成一回事。

2. List:有序、可重复、可按下标访问

List 的核心特点是:

  1. 元素有序
  2. 允许重复
  3. 支持按索引访问

最常见的实现类有:

  1. ArrayList
  2. LinkedList
  3. Vector
  4. CopyOnWriteArrayList

2.1 ArrayList

ArrayList 是业务代码里最常用的 List 实现。

特点

  1. 底层是动态数组
  2. 支持快速按索引访问
  3. 尾部追加性能通常较好
  4. 非线程安全

底层实现

可以把它理解成:一个会自动扩容的数组。

核心点包括:

  1. 内部用数组存元素
  2. 容量不够时会扩容
  3. 扩容通常是按原容量的 1.5 倍左右增长
  4. 中间位置插入或删除时,需要搬移后面的元素

如果把视角再往底层压一层,ArrayList 的核心状态其实很少:

  1. 一个真正存元素的数组
  2. 一个表示当前元素个数的 size

最值得抓住的结构可以简化成:

java
/**
 * ArrayList 的核心状态示意。
 */
public class ArrayList<E> {
    /**
     * 真正存元素的底层数组。
     */
    transient Object[] elementData;

    /**
     * 当前已经存了多少个元素。
     */
    private int size;
}

这也是为什么:

  1. get(index) 很快,因为本质上就是数组下标访问
  2. 尾部追加通常比较快,因为多数时候只是把元素写到 elementData[size]
  3. 中间插入和删除比较慢,因为后面的元素要整体搬移

例如 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);
}

这里最值得记住的是:

  1. 扩容不是每次都发生
  2. 一旦扩容,底层会复制旧数组
  3. 这也是为什么已知数据量时,提前指定容量往往更稳妥

使用场景

适合:

  1. 读多写少
  2. 频繁遍历
  3. 按下标访问
  4. 大多数普通业务列表场景

注意事项

  1. 中间插入和删除成本高,因为要搬移元素
  2. 扩容会带来数组复制开销
  3. 多线程下不能直接安全共享
  4. 如果已知数据量,最好指定初始容量,减少扩容次数

例如:

java
List<User> users = new ArrayList<>(1024);

2.2 LinkedList

LinkedList 大家都很熟,但实际业务里出现频率通常没 ArrayList 高。

特点

  1. 底层是双向链表
  2. 插入和删除节点本身比较灵活
  3. 同时实现了 ListDeque
  4. 非线程安全

底层实现

它的每个节点通常会保存:

  1. 当前元素
  2. 前驱节点引用
  3. 后继节点引用

所以它更像:一串前后相连的节点。

如果继续往下看,LinkedList 的核心不是“链表”这三个字,而是:

  1. 它是双向链表
  2. 它单独保存了 firstlast
  3. 头尾插入删除时,通常只需要修改少量引用

压缩后的核心结构可以看成这样:

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;
    }
}

链表本身通常还会保存:

  1. first:头节点
  2. last:尾节点
  3. 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++;
}

从这段代码最应该读出来的是:

  1. 头插并不需要搬移整批元素
  2. 大多数情况下只是创建一个新节点,然后改几条引用
  3. 这也是为什么说它“头尾插入删除快”

但这句话有一个很重要的前提:你操作的是头部或尾部,或者你已经拿到了目标节点。

如果你是按下标访问,例如 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;
    }
}

这也是为什么:

  1. LinkedList 理论上插删友好
  2. 但随机访问通常明显慢于 ArrayList
  3. 普通业务里如果不是频繁头尾操作,很多时候还是 ArrayList 更常见

使用场景

适合:

  1. 需要频繁在头尾插入删除
  2. 既想当 List 用,也想当队列 / 双端队列用

注意事项

  1. 按索引访问性能差,因为需要沿链表查找
  2. 节点对象多,内存开销比 ArrayList 更大
  3. 即使插入删除理论上更友好,前提也是你已经定位到目标节点
  4. 普通业务开发里,大多数场景仍然优先考虑 ArrayList

2.3 Vector

Vector 是比较老的集合实现。

特点

  1. 底层也是动态数组
  2. 方法大多带同步
  3. 线程安全粒度比较粗

使用场景

现在一般不作为首选,更多是历史知识。

如果你需要线程安全的列表,更常见的替代方案是:

  1. Collections.synchronizedList
  2. CopyOnWriteArrayList
  3. 更高层的并发控制

2.4 CopyOnWriteArrayList

这是并发场景下比较典型的列表实现。

特点

  1. 读操作基本不加锁
  2. 写操作会复制底层数组
  3. 读性能好,写成本高
  4. 迭代时看到的是快照

底层实现

核心思想是:写时复制。

也就是修改时不直接在原数组上改,而是复制出新数组再替换引用。

如果继续往下拆,CopyOnWriteArrayList 的线程安全关键不在于“神奇”,而在于:

  1. 读和写走的是两条不同路径
  2. 写操作会加锁
  3. 写完后用新数组整体替换旧数组
  4. 底层数组引用是 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];
}

这里最值得记住的是:

  1. 读线程直接读当前数组引用
  2. 不在原数组上做修改
  3. 所以读操作可以很轻

而写操作会走另一条路。以 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();
    }
}

从这段代码最应该读出来的是:

  1. 写操作不是在旧数组上原地改
  2. 而是先复制,再写新数组,再整体替换引用
  3. 旧数组仍然可以继续被正在读的线程安全使用

这也是为什么它线程安全,而且读多写少场景表现很好:

  1. 读线程彼此几乎不互相阻塞
  2. 读线程也不会和写线程争同一份可变数组
  3. 代价是每次写都要复制数组,写得越频繁,成本越高

使用场景

适合:

  1. 读多写少
  2. 配置列表
  3. 监听器列表
  4. 白名单、黑名单等更新不频繁但读取频繁的场景

注意事项

  1. 写入开销高
  2. 内存占用会放大
  3. 不能期待它适合高频写入
  4. 迭代读到的是快照,不一定是实时最新数据

2.5 一组最常见的 List 用法示例

如果你想把 List 相关实现类的日常用法一次看清,可以看下面这组示例。

这组代码重点演示:

  1. 新增元素
  2. 按位置插入
  3. 查询元素
  4. 删除元素
  5. 不同 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 的核心特点只有一句话:元素不能重复。

但“无重复”背后具体怎么实现,要看不同实现类。

最常见的有:

  1. HashSet
  2. LinkedHashSet
  3. TreeSet

3.1 HashSet

特点

  1. 元素无序
  2. 不允许重复
  3. 允许一个 null
  4. 非线程安全

底层实现

HashSet 底层其实是基于 HashMap 实现的。

你可以把它理解成:只用 key,不关心 value 的 HashMap。

也就是说:

  1. 元素作为 key
  2. 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;
    }
}

这里最值得记住的是:

  1. HashSet 的“去重”能力,本质上是 HashMap key 不能重复
  2. 元素能不能正确去重,最终还是取决于 hashCode()equals()
  3. 所以 HashSet 的很多底层特征,其实都跟 HashMap 高度一致

使用场景

适合:

  1. 去重
  2. 判断元素是否存在
  3. 对顺序没有要求的唯一性集合

注意事项

  1. 去重依赖 hashCode()equals()
  2. 如果元素参与哈希计算的字段会变,可能导致查找异常
  3. 不保证遍历顺序稳定

3.2 LinkedHashSet

特点

  1. 不允许重复
  2. 保留插入顺序
  3. 查询复杂度通常仍然接近哈希结构

底层实现

底层基于 LinkedHashMap

所以它既有哈希表定位能力,也额外维护了顺序链。

也就是说,它并不是在 HashSet 外面单独再包一层顺序结构,而是:

  1. 底层仍然用哈希结构做定位
  2. 再通过双向链表维护插入顺序

这也是为什么它能同时做到:

  1. 去重
  2. 查询效率仍然接近哈希结构
  3. 遍历结果保持稳定顺序

使用场景

适合:

  1. 既要去重
  2. 又希望保留插入顺序

例如:

  1. 标签去重后仍想按用户原始输入顺序展示
  2. 去重后的结果还要稳定输出

3.3 TreeSet

特点

  1. 不允许重复
  2. 元素有序
  3. 默认按自然顺序排序,或按比较器排序

底层实现

底层基于 TreeMap,核心结构通常是红黑树。

所以它擅长的是:

  1. 有序存储
  2. 范围查找
  3. 自动排序

从实现关系上看,TreeSetHashSet 很像,也是在“借底层 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;
    }
}

这里最关键的区别在于:

  1. HashSet 借的是 HashMap
  2. TreeSet 借的是 TreeMap
  3. 所以一个更强调哈希去重,一个更强调排序去重

使用场景

适合:

  1. 需要排序去重
  2. 需要取最小值、最大值
  3. 需要范围查询

注意事项

  1. 元素必须可比较,或者显式传入 Comparator
  2. 排序规则要和“相等性”判断保持一致,否则会出现逻辑混乱
  3. 性能通常不如基于哈希的 HashSet

3.4 线程安全的 Set

Set 这条主线里,一个很容易被遗漏的问题是:如果多个线程要同时读写同一份去重集合,应该选什么?

Set 接口本身并没有一个像 HashSet 那样默认最常用的并发实现,通常要根据读写模型来选。

最常见的几种方案可以记成这张表:

方案核心特点更适合什么场景需要注意什么
Collections.synchronizedSet(new HashSet<>())用同步包装普通 Set并发不高、改造老代码迭代时通常仍要手动同步
CopyOnWriteArraySet写时复制;读线程读快照读多写少、监听器、白名单写入成本高,不适合高频写
ConcurrentHashMap.newKeySet()基于 ConcurrentHashMap 的 key 视图;无序;并发写友好在线用户、任务去重、共享状态集合迭代是弱一致性视图,不是静态快照
ConcurrentSkipListSet基于跳表;线程安全;元素有序并发有序去重、范围查询不允许 null,性能取舍和哈希结构不同

如果只想先抓最实用的选择方式,可以直接这样记:

  1. 并发量不高,先求简单:Collections.synchronizedSet(...)
  2. 读很多、写很少:CopyOnWriteArraySet
  3. 并发写也很多,而且不要求顺序:ConcurrentHashMap.newKeySet()
  4. 并发下还要保持有序: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 最关键的主线是“去重”,但不同实现类对顺序和排序的支持不一样。

下面这组代码重点演示:

  1. 新增元素
  2. 自动去重
  3. 判断元素是否存在
  4. 删除元素
  5. 插入顺序和排序语义
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 更像“按规则取用元素的容器”。

常见类型包括:

  1. Queue
  2. Deque
  3. PriorityQueue
  4. ArrayDeque

4.1 Queue

Queue 更偏队列语义,通常是:先进先出(FIFO)。

常见操作包括:

  1. offer
  2. poll
  3. peek

它适合表达:

  1. 排队
  2. 消费
  3. 调度

4.2 Deque

Deque 是双端队列。

它允许:

  1. 头部插入和删除
  2. 尾部插入和删除

所以它既可以当队列,也可以当栈来用。

4.3 ArrayDeque

这是很值得记的一个实现类。

特点

  1. 底层通常是循环数组
  2. 头尾操作都比较高效
  3. 既能当队列,也能当栈
  4. 不允许存 null

底层实现

ArrayDeque 最值得建立的直觉是:它不是普通线性数组,而是一个首尾相接的循环数组。

它通常会维护:

  1. elements:真正存元素的数组
  2. head:当前头部位置
  3. 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;
}

这也是为什么它的头尾操作通常很高效:

  1. 不需要像 ArrayList 那样搬移大量元素
  2. 大多数时候只是改下标和写数组槽位
  3. 这也让它非常适合表达队列、栈和双端队列语义

使用场景

适合:

  1. 普通队列
  2. 双端队列
  3. 栈结构替代 Stack

注意事项

  1. 如果只是想用栈,通常更推荐 ArrayDeque 而不是老的 Stack
  2. 不能放 null

4.4 PriorityQueue

PriorityQueue 不是普通 FIFO 队列,而是优先级队列。

特点

  1. 底层通常是堆,常见是小顶堆
  2. 每次取出的都是优先级最高或最低的元素
  3. 不保证整体遍历就是完全有序

底层实现

PriorityQueue 的底层重点不是“队列”,而是:它内部通常是用数组表示的一棵二叉堆。

默认场景下更常见的是:小顶堆。

也就是:

  1. 堆顶元素最小
  2. 每次 poll() 拿到的是当前最小值
  3. 但整个数组并不是完全排好序的

压缩后的核心结构可以理解成这样:

java
/**
 * PriorityQueue 的核心状态示意。
 */
public class PriorityQueue<E> {
    /**
     * 真正存堆节点的数组。
     */
    transient Object[] queue;

    /**
     * 当前元素个数。
     */
    int size;
}

插入元素的核心思路通常是:

  1. 把元素放到数组尾部
  2. 再一路向上比较
  3. 必要时和父节点交换位置

压缩后的 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;
}

而删除堆顶元素的核心思路通常是:

  1. 取出堆顶
  2. 把最后一个元素放到堆顶
  3. 再一路向下比较并调整

这也是为什么它非常适合:

  1. 每次只关心“当前优先级最高的是谁”
  2. 不要求整个集合随时保持完全有序
  3. 需要反复取最值的场景

使用场景

适合:

  1. Top K
  2. 定时调度
  3. 最值维护
  4. 贪心算法和图算法

注意事项

  1. 它保证的是队头优先,不是整体完全排序
  2. 如果要完整有序结果,通常需要反复 poll
  3. 元素需要可比较,或提供比较器

4.5 一组最常见的 Queue / Deque 用法示例

队列相关集合最值得先熟悉的是:

  1. offer:放入元素
  2. poll:取出并删除队头元素
  3. peek:只查看队头元素,不删除
  4. 双端队列的头尾操作
  5. 优先级队列的优先出队语义
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 是业务中使用频率最高的集合之一。

常见实现类包括:

  1. HashMap
  2. LinkedHashMap
  3. TreeMap
  4. Hashtable
  5. ConcurrentHashMap
  6. EnumMap

5.1 HashMap

HashMap 是最容易被反复展开的主题之一。

特点

  1. 允许一个 null key
  2. 允许多个 null value
  3. 查询和插入平均性能较好
  4. 非线程安全

底层实现

JDK 8 以后,通常可以概括成:数组 + 链表 + 红黑树

大致过程是:

  1. 先根据 key 的哈希值定位桶位置
  2. 如果桶里没有冲突,直接放入
  3. 如果有冲突,先挂链表
  4. 冲突严重时,链表可能树化成红黑树

你至少要记住两个核心点:

  1. 哈希表负责快速定位
  2. 树化是为了在冲突严重时优化性能

如果继续往下看,HashMap 最核心的底层状态通常包括:

  1. table:桶数组
  2. size:当前键值对数量
  3. threshold:触发扩容的阈值
  4. loadFactor:负载因子

压缩后的核心结构可以理解成这样:

java
/**
 * HashMap 的核心状态示意。
 */
public class HashMap<K, V> {
    /**
     * 哈希桶数组。
     * 每个桶位上可能挂链表或红黑树。
     */
    transient Node<K, V>[] table;

    /**
     * 当前实际元素个数。
     */
    transient int size;

    /**
     * 触发扩容的阈值。
     */
    int threshold;

    /**
     * 负载因子,默认常见值是 0.75。
     */
    final float loadFactor;
}

插入元素时,核心流程通常可以压缩成下面几步:

  1. 先算 key 的哈希值
  2. 再定位桶位
  3. 桶为空就直接放
  4. 桶不为空就处理冲突
  5. 冲突链过长时可能树化
  6. 元素总量达到阈值时触发扩容

压缩后的 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;
}

这里最值得建立的直觉是:

  1. HashMap 快,不是因为它“魔法快”
  2. 而是因为它通过哈希把查找范围先缩小到某个桶
  3. 真正拖慢它的通常是哈希冲突和扩容

扩容时最关键的成本在于:

  1. 新建更大的桶数组
  2. 重新分布旧节点
  3. 这也是为什么大量写入场景下,初始容量设置不合理会带来明显额外成本

使用场景

适合:

  1. 按 key 快速查值
  2. 计数
  3. 缓存映射
  4. 各类业务状态映射

注意事项

  1. key 最好是不可变对象
  2. 自定义 key 时要正确重写 hashCode()equals()
  3. 多线程下不能直接安全使用
  4. 不要把它的遍历顺序当成稳定顺序

5.2 LinkedHashMap

特点

  1. 保留插入顺序
  2. 也可以按访问顺序维护
  3. 查找性能仍然保留哈希结构的优势

底层实现

它是在 HashMap 基础上增加了双向链表来维护顺序。

如果从结构上理解,LinkedHashMap 可以看成:HashMap + 双向链表。

也就是说,它不是推翻了哈希结构,而是在每个节点上额外保存顺序指针。

压缩后的节点结构可以理解成这样:

java
/**
 * LinkedHashMap 节点结构示意。
 */
static class Entry<K, V> extends HashMap.Node<K, V> {
    /**
     * 顺序链上的前驱节点。
     */
    Entry<K, V> before;

    /**
     * 顺序链上的后继节点。
     */
    Entry<K, V> after;
}

这也是为什么它能做到两件事同时成立:

  1. 查找仍然主要依赖哈希定位
  2. 遍历顺序则由双向链表来保证

如果开启 accessOrder = true,那么每次访问节点后,核心思路还会把该节点移动到链表尾部。

这也是为什么它很适合做简易 LRU:最近访问的节点会不断往后移动,最久没访问的节点会留在头部。

使用场景

适合:

  1. 既要按 key 快速查找
  2. 又要稳定遍历顺序
  3. 简单 LRU 缓存实现

经典点在于它可以配合 removeEldestEntry 做简易 LRU。

5.3 TreeMap

特点

  1. 按 key 有序
  2. 支持自然排序和比较器排序
  3. 支持范围查询

底层实现

底层通常是红黑树。

如果再往下一层看,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;
}

插入时最核心的流程通常是:

  1. 从根节点开始比较 key
  2. 小于走左子树,大于走右子树
  3. 找到空位插入新节点
  4. 再执行红黑树平衡调整

所以它能支持:

  1. 自动排序
  2. 范围查询
  3. 向上取整、向下取整、最值查询

但代价也很明显:

  1. 每次插入和删除都不是简单数组操作
  2. 还要维护树的平衡
  3. 所以普通点查性能通常不如 HashMap

使用场景

适合:

  1. 需要按 key 排序
  2. 需要区间查询
  3. 需要最小 key、最大 key、向上取整、向下取整这类能力

注意事项

  1. key 必须可比较,或显式提供比较器
  2. 性能通常不如 HashMap
  3. 比较规则要稳定且符合业务语义

5.4 Hashtable

这是比较老的线程安全 Map 实现。

特点

  1. 方法大多直接同步
  2. 不允许 null key
  3. 不允许 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);
}

这也是为什么:

  1. 它线程安全
  2. 但同步粒度比较粗
  3. 高并发下吞吐通常不如 ConcurrentHashMap

使用场景

现在更多是历史知识,一般不作为新代码首选。

5.5 ConcurrentHashMap

这是并发场景里最重要的 Map 实现之一。

特点

  1. 线程安全
  2. 并发性能明显好于粗粒度整表锁
  3. 不允许 null key
  4. 不允许 null value

底层实现

JDK 8 中常见的理解是:

  1. 仍然有哈希桶结构
  2. 结合 CAS
  3. 结合更细粒度的同步控制

所以它不是像 Hashtable 那样简单地把整张表全部锁住。

如果继续往下一层拆,ConcurrentHashMap 的核心状态通常会包含:

  1. table:桶数组
  2. sizeCtl:初始化和扩容控制字段
  3. 桶位上的节点链 / 树

最值得建立的判断是:它不是整表一把大锁,而是尽量让无冲突路径更轻,冲突时再进入局部同步。

压缩后的核心状态可以理解成这样:

java
/**
 * ConcurrentHashMap 的核心状态示意。
 */
public class ConcurrentHashMap<K, V> {
    /**
     * 哈希桶数组。
     * 使用 volatile 是为了让数组引用和桶位变更对其他线程可见。
     */
    transient volatile Node<K, V>[] table;

    /**
     * 控制初始化、扩容阈值等行为的状态字段。
     */
    private transient volatile int sizeCtl;
}

插入时的核心思路通常可以压缩成下面几步:

  1. 先算哈希并定位桶位
  2. 桶为空时优先尝试 CAS 放入
  3. 桶不为空时,对该桶进入同步流程
  4. 链过长时可能树化

压缩后的 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;
}

这里最值得读出来的是:

  1. 无冲突时尽量走 CAS
  2. 冲突时只锁当前桶,不锁整张表
  3. 这也是为什么它的并发性能通常明显好于 Hashtable

它为什么线程安全,也可以压缩成三点:

  1. 写入时用 CAS 和局部同步控制并发修改
  2. 关键状态用 volatile 保证可见性
  3. 复合操作需要优先使用 putIfAbsentcomputeIfAbsent 这类原子方法

使用场景

适合:

  1. 多线程共享缓存
  2. 并发计数和状态映射
  3. 高并发读写场景下的键值存储

注意事项

  1. 不能存 null
  2. 线程安全不代表复合操作天然原子
  3. 像“先判断再更新”这类逻辑,仍要考虑并发一致性

比如:

java
map.putIfAbsent(key, value);
map.computeIfAbsent(key, k -> load(k));

这类原子方法往往比你手动 getput 更稳妥。

5.6 EnumMap

这是一个很容易被忽略,但非常实用的实现类。

特点

  1. key 必须是枚举类型
  2. 性能和空间利用率通常都很好
  3. 语义非常清晰

底层实现

因为枚举值集合固定,所以它可以基于更紧凑的结构实现,通常比通用 HashMap 更高效。

如果从数据结构上看,EnumMap 最值得记住的是:它并不需要像 HashMap 那样去做哈希定位。

因为枚举值集合是固定的,所以它通常会:

  1. 先拿到枚举常量数组
  2. 再通过 ordinal() 直接定位槽位

压缩后的核心结构可以理解成这样:

java
/**
 * EnumMap 的核心状态示意。
 */
public class EnumMap<K extends Enum<K>, V> {
    /**
     * 当前枚举类型的所有常量。
     */
    private transient K[] keyUniverse;

    /**
     * 按枚举 ordinal 存放 value 的数组。
     */
    private transient Object[] vals;
}

这也是为什么它通常:

  1. 结构更紧凑
  2. 访问路径更直接
  3. 在“固定枚举 -> 值”这类场景下很自然

使用场景

适合:

  1. 用枚举作为状态键
  2. 固定类别映射

例如:

java
EnumMap<OrderStatus, String> descMap = new EnumMap<>(OrderStatus.class);

5.7 一组最常见的 Map 用法示例

如果你想把最常见的 Map 操作一次串起来,可以重点记这些 API:

  1. put
  2. get
  3. containsKey
  4. remove
  5. 并发场景下的 putIfAbsentcomputeIfAbsent
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 不只是有普通集合,还有专门面向并发场景的集合。

最常见的代表包括:

  1. ConcurrentHashMap
  2. CopyOnWriteArrayList
  3. CopyOnWriteArraySet
  4. ConcurrentSkipListSet
  5. BlockingQueue

6.1 BlockingQueue

它最典型的能力不是“存元素”,而是:在队列空或满时,支持阻塞式生产消费。

常见实现包括:

  1. ArrayBlockingQueue
  2. LinkedBlockingQueue
  3. PriorityBlockingQueue

底层实现

BlockingQueue 是接口,不是某一个固定实现。

如果想先建立最核心的直觉,最适合抓 ArrayBlockingQueue 这类典型实现来看。

它的底层重点通常包括:

  1. 一个真正存元素的数组
  2. 一把保护入队和出队的锁
  3. 两个条件队列:notEmptynotFull

压缩后的核心结构可以理解成这样:

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 的核心思路则刚好相反:

  1. 队列空时等待 notEmpty
  2. 取出元素后唤醒等待放入的线程

这也是为什么 BlockingQueue 适合生产者消费者模型:

  1. 共享队列本身负责缓存任务
  2. 队列满和队列空时的等待逻辑已经被内建进去
  3. 业务代码通常不需要自己手写 wait/notify

使用场景

适合:

  1. 生产者消费者模型
  2. 线程池任务队列
  3. 异步任务处理

注意事项

  1. 容量策略会影响系统背压
  2. 无界队列不是绝对安全,可能带来内存压力

6.2 BlockingQueue 用法示例

BlockingQueue 最值得先掌握的不是底层实现,而是几组常见操作:

  1. put:队列满时会等待
  2. take:队列空时会等待
  3. offer:尝试放入
  4. 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 如果你需要“有序、可重复、按下标访问”

优先想:

  1. ArrayList

7.2 如果你需要“唯一性”

优先想:

  1. HashSet
  2. 如果还要保留插入顺序,用 LinkedHashSet
  3. 如果还要排序,用 TreeSet

7.3 如果你需要“按 key 快速查值”

优先想:

  1. HashMap
  2. 如果还要顺序,用 LinkedHashMap
  3. 如果还要排序,用 TreeMap
  4. 如果是并发场景,用 ConcurrentHashMap

7.4 如果你需要“队列 / 栈 / 双端队列”

优先想:

  1. ArrayDeque
  2. 如果要优先级,用 PriorityQueue
  3. 如果要阻塞消费,用 BlockingQueue

8. 高频注意事项

8.1 hashCode()equals() 必须协同

凡是放进 HashMapHashSet 这类哈希集合中的对象,都要特别注意:

  1. 如果重写了 equals(),通常也要重写 hashCode()
  2. 否则会出现逻辑相等但无法正确查找或去重的问题

8.2 不要随便修改作为 key 的对象状态

如果一个对象已经作为 HashMap 的 key 放进去,而你又修改了参与哈希计算的字段,就可能导致:

  1. 找不回来
  2. 删除失败
  3. 逻辑错乱

所以更稳妥的做法是:把 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,而是一个固定长度的列表视图。

所以:

  1. 可以改元素
  2. 不能随便 addremove

8.5 subList() 是视图,不是完全独立副本

subList() 的修改,往往会影响原列表,反过来也可能受原列表结构变化影响。

如果你要的是独立列表,通常应该显式复制一份。

8.6 null 支持情况不要想当然

不同集合对 null 的支持不一样:

  1. ArrayList 允许 null
  2. HashMap 允许一个 null key
  3. ConcurrentHashMap 不允许 null
  4. ArrayDeque 不允许 null

这块也很容易被忽略。

9. 一段更适合放进正文里的总结说明

如果把这个问题压缩成一段完整说明“Java 集合类型有哪些,各有什么特点”,可以这样回答:

Java 集合框架大体可以分成 CollectionMap 两条线。Collection 下面主要有 ListSetQueueList 强调有序和可重复,典型实现是 ArrayListLinkedListSet 强调唯一性,常见有 HashSetLinkedHashSetTreeSetQueueDeque 更强调队列、双端队列和优先级队列语义,常见有 ArrayDequePriorityQueueMap 主要是键值映射,最常见的是 HashMapLinkedHashMapTreeMapConcurrentHashMap。底层实现上,ArrayList 是动态数组,LinkedList 是双向链表,HashMap 是哈希桶加链表 / 红黑树,TreeMapTreeSet 通常基于红黑树。实际选型时,要根据是否需要顺序、唯一性、排序、并发安全、随机访问和读写比例来判断。

10. 最后给一个简单选型表

如果你想快速记忆,可以记这张表:

  1. 读多、按下标访问多:ArrayList
  2. 去重:HashSet
  3. 去重且保留插入顺序:LinkedHashSet
  4. 去重且排序:TreeSet
  5. 普通键值映射:HashMap
  6. 映射且保留顺序:LinkedHashMap
  7. 映射且按 key 排序:TreeMap
  8. 并发映射:ConcurrentHashMap
  9. 栈 / 双端队列:ArrayDeque
  10. 优先级队列:PriorityQueue

真正理解集合,不是背谁底层是数组、谁底层是链表,而是:

看到业务需求后,能立刻反推出应该选哪一类集合,以及为什么。

基于 VitePress 构建的个人技术笔记。