Java 常用的集合详解(源码分析等部分 持续补充中)

省流版

集合底层机制特性扩容机制线程安全优点
ArrayList动态数组允许存储重复元素和 null 值。随机访问较快,插入删除较慢当元素数量超过当前数组容量时,ArrayList 会自动扩容,新容量 = 旧容量 + 旧容量 / 2(即扩容为原来的 1.5 倍),如果扩容后的容量仍然不足,则直接使用所需的最小容量,将旧数组中的元素复制到新数组中。其动态扩展、快速随机访问以及灵活的增删操作的优点,适合需要频繁访问元素且数据量动态变化的场景。
LinkedList双向链表插入和删除操作上具有更高的效率,但在随机访问上性能较差没有固定的初始容量,需要像 ArrayList 那样进行扩容操作插入和删除操作高效,尤其在列表中间进行增删时性能优异,因为只需调整指针而无需移动元素。此外,它还支持双向遍历,适合需要频繁从两端访问或修改数据的场景。
Vector动态数组允许存储重复元素和 null 值,随机访问较快如果指定了扩容增量(capacityIncrement),则新容量 = 旧容量 + 扩容增量; 如果未指定扩容增量,则新容量 = 旧容量 * 2;如果扩容后的容量仍然不足,则直接使用所需的最小容量;将旧数组中的元素复制到新数组中。线程安全,所有操作方法都是同步的,适合在多线程环境中使用。此外,它支持动态扩展,能够根据需要自动调整容量,简化了数组大小管理的复杂性。
StackVector实现的栈数据结构后进先出(LIFO, Last In First Out扩容机制与 Vector 相同遵循后进先出(LIFO)原则,适合需要处理栈式数据结构的场景,如表达式求值、回溯算法等。此外,它基于 Vector 实现,继承了线程安全特性,能够在多线程环境下安全操作栈元素。
CopyOnWriteArrayList基于写时复制(Copy-On-Write)机制实现读操作性能极高,写操作性能较低CopyOnWriteArrayList 的扩容机制与 ArrayList 类似,每次写操作都会创建一个新的数组副本。新数组的容量根据实际需求动态调整。如果新数组容量不足,则扩容为当前容量的 1.5 倍读操作上完全线程安全且无需加锁,适合高读低写的并发场景,因为写操作通过复制底层数组实现,避免了对原始列表的直接修改。此外,它简化了多线程环境下的迭代操作,不会抛出ConcurrentModificationException,确保迭代器的安全性。
Collections.synchronizedList通过包装现有的List实现类(如ArrayListLinkedList)来提供线程安全的功能。它内部维护了一个mutex对象(锁对象),所有方法都通过synchronized(mutex)实现同步。线程安全扩容机制取决于被包装的 List 实现类为现有列表提供线程安全的包装,适合在多线程环境中对已有列表进行同步操作,而无需替换原有数据结构。此外,它比直接使用 Vector 更灵活,允许开发者根据需求选择性地同步特定代码块,提升性能和可控性。
HashSetHashSet的底层是一个HashMap,元素存储在HashMap的键中,值是一个固定的PRESENT对象。HashSet不允许存储重复元素,并且不保证元素的顺序。允许存储null值。当元素数量超过 当前容量 * 负载因子时,HashSet 会自动扩容。新容量 = 旧容量 * 2基于哈希表实现,添加、删除和查找元素的平均时间复杂度为 O(1),适合需要快速判断元素是否存在或进行集合运算的场景。此外,它不允许存储重复元素,自动保证集合中元素的唯一性。
LinkedHashSetLinkedHashSet的底层是一个LinkedHashMap,结合了哈希表和双向链表的特性,哈希表用于快速查找和去重,双向链表用于维护元素的插入顺序LinkedHashSet不允许存储重复元素,且允许存储null值。它保留了元素的插入顺序当元素数量超过当前容量与负载因子的乘积时,LinkedHashSet会自动扩容,新容量 = 旧容量 * 2继承了HashSet的高效去重能力,同时通过双向链表维护元素的插入顺序,适合需要兼顾元素唯一性和顺序性的场景。此外,它的迭代顺序与插入顺序一致,便于有序遍历和操作。
TreeSet基于红黑树(Red-Black Tree,一种自平衡二叉搜索树)TreeSet不允许存储重复元素,并且元素按照自然顺序或自定义顺序排序, 不允许存储 null值(除非使用自定义比较器)TreeSet基于红黑树实现,不需要预先分配内存空间,因此没有固定的初始容量。每次添加元素时,动态调整红黑树的结构以保持平衡。基于红黑树实现,能够自动对元素进行排序,适合需要维护有序集合的场景,插入、删除和查找操作的时间复杂度为 O(log n)。此外,它支持范围查询和获取最小/最大值等操作,功能强大且高效。
CopyOnWriteArraySet基于写时复制(Copy-On-Write)机制实现线程安全,所有写操作都通过加锁保证线程安全,读操作不需要加锁。CopyOnWriteArrayList相同,每次写操作都会创建一个新的数组副本。新数组的容量根据实际需求动态调整。如果新数组容量不足,则扩容为当前容量的 1.5 倍。基于CopyOnWriteArrayList实现,提供线程安全的集合操作,特别适合高读低写的并发场景,读操作无需加锁且不会抛出ConcurrentModificationException。此外,它通过写时复制机制确保数据一致性,避免了传统集合在多线程环境下的并发修改问题。
ConcurrentSkipListSet基于 跳表(Skip List) 实现不允许存储重复元素,且不允许存储null值,支持元素的有序性和高效的并发操作基于跳表实现,不需要预先分配内存空间,因此没有固定的初始容量。每次添加元素时,动态调整跳表的结构以保持平衡。提供高度并发的有序集合操作,基于跳表结构实现,支持高效的并发插入、删除和查找操作,时间复杂度为 O(log n)。此外,它在多线程环境下表现出色,无需锁整个数据结构即可保证线程安全,适合高并发场景下的有序集合需求。
EnumSet基于 位向量(bit vector 实现性能极高,且内存占用非常低。 不允许存储null值,且只能存储同一枚举类型的元素EnumSet不需要动态扩容,因为枚举类型的常量数量是固定的。专门为枚举类型设计,内存占用极小且性能高效,适合操作枚举常量集合的场景。此外,它强制只能包含指定枚举类型的元素,确保了类型安全和集合的语义清晰性。
Collections.synchronizedSet通过包装现有的 Set实现类(如HashSet或TreeSet)来提供线程安全的功能。它内部维护了一个 mutex对象(锁对象),所有方法都通过synchronized(mutex)实现同步为所有方法添加同步锁(synchronized)来保证线程安全取决于其包装的集合现有集合提供线程安全的包装,适合在多线程环境中使用,而无需替换原有集合实现。此外,它通过同步方法确保操作的安全性,同时允许开发者手动控制同步块以优化性能。
PriorityQueuePriorityQueue的底层是一个Object[]数组,用于存储元素。使用堆(Heap)数据结构实现,通常是一个最小堆(Min-Heap)或最大堆(Max-HeapPriorityQueue允许存储元素,并且元素按照自然顺序或自定义顺序排序,不允许存储null 值。如果当前容量小于64,则新容量 = 旧容量 * 2。如果当前容量大于等于64,则新容量 = 旧容量 * 1.5。PriorityQueue的优点是基于堆结构实现,能够高效地维护元素的优先级顺序,插入和删除操作的时间复杂度为O(log n),适合需要动态处理优先级队列的场景。此外,它自动对元素进行排序(可自定义比较规则),简化了优先级管理的复杂性。
ArrayDequeArrayDeque的底层是一个Object[]数组,用于存储元素。使用两个指针(headtail)来标记队列的头部和尾部允许在队列的两端插入和删除元素,支持栈(LIFO)和队列(FIFO)的操作,不允许存储null默认初始容量为 16,当元素数量超过当前容量时,ArrayDeque会自动扩容,新容量 = 旧容量 * 2。基于数组实现的双端队列,支持在两端高效地进行插入和删除操作,时间复杂度为O(1),适合需要频繁从两端操作数据的场景。此外,它比LinkedList更节省内存,并且在大多数情况下性能更优,适合作为栈或队列的实现选择。
ConcurrentLinkedQueue基于链表实现的无界线程安全队列采用非阻塞算法(CAS,Compare-And-Swap)实现高并发场景下的线程安全,无锁设计避免了线程阻塞,吞吐量高。遵循先进先出(FIFO)原则没有显式扩容机制基于无锁算法实现的线程安全队列,提供高效的并发性能,适合高并发场景下的非阻塞操作。此外,作为FIFO队列,它支持原子性的插入和移除操作,确保数据一致性的同时避免了传统锁机制带来的性能开销。
BlockingQueueJava并发包(java.util.concurrent)中定义的接口,代表一个线程安全的队列,支持阻塞操作主要用于生产者 - 消费者模型,允许线程在队列满或空时自动阻塞或等待,直到条件满足提供线程安全的阻塞队列,支持生产者-消费者模式下的高效并发操作,在队列为空或满时能够自动阻塞线程,简化多线程编程复杂性。此外,它有多种实现(如ArrayBlockingQueueLinkedBlockingQueue),适用于不同场景,确保灵活且高效的资源管理。
ArrayBlockingQueue内部使用Object[] items存储元素,通过两个指针putIndex(入队位置)和takeIndex(出队位置)实现循环数组先进先出(FIFO),有界队列容量在初始化时固定,无法动态扩展基于固定大小数组实现的阻塞队列,提供线程安全的FIFO操作,适合需要限制队列容量以避免无界增长的场景。此外,它的性能较高且内存占用固定,通过阻塞机制简化了生产者-消费者模型中的同步问题。
LinkedBlockingQueue基于链表实现的可选有界阻塞队列先进先出,初始化时可指定容量,若未指定则默认为Integer.MAX_VALUE初始化时指定容量,队列满后无法动态扩容,若未指定容量(即capacity = Integer.MAX_VALUE),理论上可无限添加元素(受内存限制)线程安全且高效,适合高并发场景。提供丰富的阻塞和超时方法,便于实现复杂的线程间协作。可选容量上限,灵活性较高。
PriorityBlockingQueue基于优先级堆实现的无界阻塞队列元素按自然顺序(Comparable)或自定义Comparator排序当插入元素时,若当前数组已满,触发扩容。若当前容量 < 64,新容量 = 旧容量 * 2 + 2;若当前容量 ≥ 64,新容量 = 旧容量 * 1.5。线程安全且高效,适合高并发场景。支持优先级排序,便于实现任务调度或资源分配。动态扩展容量,灵活性较高。
SynchronousQueue无存储结构本身不存储元素,每个插入操作必须等待一个对应的移除操作不存储元素,容量始终为 0,无需扩容线程安全且高效,适合直接传递模型。没有存储开销,性能较高。简化了生产者和消费者之间的交互逻辑。
DelayQueue基于优先级堆实现的无界阻塞队列元素按延迟时间排序,最早到期的元素优先出队,元素必须实现 Delayed 接口,指定延迟时间当插入元素时,若当前数组已满,触发扩容,与PriorityQueue相同,容量扩展为原大小的 50%线程安全且高效,适合高并发场景。支持延迟任务调度,便于实现任务延时执行。动态扩展容量,灵活性较高。
HashMap一个数组 + 链表/红黑树的组合数据结构允许存储键值对,并且键和值都可以为null,不保证元素的顺序元素数量超过当前容量与负载因子的乘积时,会自动扩容,新容量 = 旧容量 * 2基于哈希表实现,提供平均时间复杂度为O(1) 的快速键值对插入、删除和查找操作,适合需要高效数据检索的场景。此外,它允许存储 null 键和 null 值,并且底层动态扩容机制简化了容量管理。
LinkedHashMap底层是一个HashMap,同时维护了一个双向链表不仅具有HashMap的快速查找特性,还保留了元素的插入顺序或访问顺序。允许存储键值对,键和值都可以为null完全继承自HashMa继承了HashMap的高效键值对操作(平均时间复杂度 O(1)),同时通过双向链表维护元素的顺序,支持按插入顺序或访问顺序迭代。此外,它非常适合实现缓存机制(如 LRU 缓存),在需要有序性和高性能的场景中表现优异。
TreeMapSortedMap接口的实现类,基于红黑树(Red-Black Tree)实现的,一种自平衡二叉搜索树不允许存储重复键,且键不允许为null(除非使用自定义比较器),并且键按照自然顺序或自定义顺序排序,插入、删除和查找操作的时间复杂度为O(log n)基于红黑树实现,不需要预先分配内存空间基于红黑树实现,能够自动对键进行排序,支持有序键值对存储和范围查询,插入、删除、查找操作的时间复杂度为O(log n)。此外,它提供了获取最小键、最大键及键的前驱和后继等功能,适合需要有序映射的场景。
ConcurrentHashMapJDK 1.8:使用数组 + 链表 + 红黑树的结构高并发环境下提供了高效的读写操作,适合多线程场景。基于分段锁(Segment Locking)或 CAS(Compare-And-Swap)机制实现。它允许存储键值对,键和值都不允许为null当元素数量超过当前容量与负载因子的乘积时,ConcurrentHashMap 会自动扩容,新容量 = 旧容量 * 2提供高度线程安全的并发哈希表实现,通过分段锁或无锁机制确保高并发环境下的性能,适合多线程场景下的键值对存储。此外,它支持高效的读操作且无需加锁,同时提供了丰富的原子操作方法(如 computeIfAbsent),简化了并发编程复杂性。
Hashtable基于哈希表实现,底层是一个Entry<?,?>[]数组,每个Entry是一个键值对是 Java 中一个古老的集合类,允许存储键值对,键和值都不允许为null,它不保证元素的顺序,线程安全,但性能较低元素数量超过当前容量与负载因子的乘积时,会自动扩容,新容量 = 旧容量 * 2 + 1线程安全的哈希表实现,所有方法均同步,适合早期 Java 版本中的多线程环境。此外,它不允许存储 null 键或 null 值,确保键值对的完整性,适合需要严格数据约束的场景。
WeakHashMap底层是一个Entry<?,?>[]数组,每个Entry是一个键值对键是弱引用,当键不再被强引用时,垃圾回收器会自动回收这些键值对当元素数量超过当前容量与负载因子的乘积时,会自动扩容,新容量 = 旧容量 * 2基于弱引用实现键值对存储,当键不再被外部引用时,键值对可被垃圾回收机制自动清理,适合需要管理临时对象或避免内存泄漏的场景。此外,它在处理缓存或映射关系时能够动态释放资源,降低了手动管理内存的复杂性。
EnumMap基于枚举常量的顺序实现,底层是一个Object[]数组,数组的索引对应枚举常量的顺序专门用于枚举类型的高效集合类,性能极高,且内存占用非常低。EnumMap允许存储键值对,键必须是同一枚举类型的常量,值可以是任意类型不需要动态扩容,因为枚举类型的常量数量是固定的专为枚举类型键设计,内部使用数组实现,提供接近 O(1) 的高效存取性能,且内存占用极小。此外,它自动按照枚举常量的声明顺序存储键值对,确保迭代顺序一致性,适合需要操作枚举类型映射的场景。
IdentityHashMap底层是一个Object[]数组,数组的每个元素交替存储键和值基于对象引用相等性(==)而不是对象值相等性(equals)实现的哈希表,键和值都可以为null,使用System.identityHashCode来计算哈希值,用开放地址法(Open Addressing)解决哈希冲突元素数量超过当前容量与负载因子的乘积时,会自动扩容,新容量 = 旧容量 * 2基于引用相等性(==)而非equals方法比较键和值,适合需要精确控制对象身份的场景,避免了重写 equalshashCode方法带来的影响。此外,它在处理循环引用或特殊对象映射时表现优异,确保数据结构的稳定性和准确性。
Collections.synchronizedMap通过包装现有的Map实现类来提供线程安全的功能为其包装现有的Map实现类的所有方法添加同步锁,从而保证线程安全,性能较低,在高并发场景下,可能会成为性能瓶颈取决于包装的实现类为现有 Map 提供线程安全的包装,适合在多线程环境中使用,而无需替换原有实现。此外,它通过同步方法确保操作的安全性,并允许开发者手动控制同步块以优化性能,灵活性较高。

一、基础了解

1.1、概念

Java集合框架(Java Collections Framework)是Java标准库中用于存储和操作数据集合的核心组件。它提供了一套丰富的接口、实现类和算法,用于处理各种数据结构(如列表、集合、映射等)。

1.2、体系

1.2.1、概述

Java 集合体系的核心接口和类位于java.util包中,主要分为两大类:

  • Collection:存储单值元素的集合,所有单值集合类的根接口,定义了集合的基本操作。
    • 核心方法:
      • add(E e):添加元素。
      • remove(Object o):删除元素。
      • size():返回集合大小。
      • iterator():返回迭代器。
    • 子类:
      • List:有序集合,允许重复元素。
      • Set:无序集合,不允许重复元素。
      • Queue:队列,支持先进先出(FIFO)或优先级排序。
  • Map:存储键值对的集合,键唯一(基于equals()hashCode()方法判断键是否重复),值可以重复。
    • 核心方法:
      • put(K key, V value):添加键值对。
      • get(Object key):根据键获取值。
      • remove(Object key):删除键值对。
      • keySet():返回所有键的集合。
      • values():返回所有值的集合。

1.2.2、图解

在这里插入图片描述

1.3、适用场景总结

集合类型特点适用场景
ArrayList动态数组,支持随机访问,插入删除较慢。需要频繁访问元素的场景。
LinkedList双向链表,插入删除较快,随机访问较慢。需要频繁插入删除元素的场景。
HashSet基于哈希表,元素无序,不允许重复。需要快速查找且不关心元素顺序的场景。
TreeSet基于红黑树,元素有序,不允许重复。需要元素排序的场景。
HashMap基于哈希表,键值对无序,允许 null 键和值。需要快速查找键值对的场景。
TreeMap基于红黑树,键值对有序。需要键值对排序的场景。
PriorityQueue基于堆,元素按优先级排序。需要优先级队列的场景。

二、List 接口及其实现类

2.1、概念

ListJava集合框架中的一个核心接口,表示有序的集合(也称为序列)。List允许存储重复的元素,并且可以通过索引访问元素。

  • 特点
    • 有序性:元素按照插入顺序存储,可以通过索引访问。
    • 允许重复元素:可以存储多个相同的元素。
    • 允许 null 元素:可以存储 null 值。
  • 核心方法
    • add(E e):在列表末尾添加元素。
    • add(int index, E element):在指定位置插入元素。
    • get(int index):获取指定位置的元素。
    • remove(int index):删除指定位置的元素。
    • set(int index, E element):替换指定位置的元素。
    • size():返回列表的大小。
    • iterator():返回列表的迭代器。
  • 核心实现类:
    • 非线程安全
      • ArrayList:基于动态数组实现,支持随机访问。
      • LinkedList:基于双向链表实现,支持高效插入和删除。
    • 线程安全
      • Stack:基于 Vector 实现,表示后进先出(LIFO)的栈。
      • Vector:线程安全的动态数组(已过时,推荐使用 Collections.synchronizedList)。
      • CopyOnWriteArrayList:基于写时复制(Copy-On-Write)机制实现。读操作不加锁,写操作加锁并复制新数组。适合读多写少的场景。
  • Collections.synchronizedList:通过包装非线程安全的 List 实现线程安全,所有方法都使用 synchronized 修饰。

2.2、ArrayList详解

2.2.1、概述

ArrayList是 Java 中最常用的集合类之一,它基于动态数组实现,提供了高效的随机访问和动态扩容能力。ArrayList 允许存储重复元素和 null 值。随机访问(通过索引)的时间复杂度为 O(1)。ArrayList是线程不安全的,如果多个线程同时修改ArrayList,可能会导致数据不一致或抛出异常。

  • ArrayList的底层是一个 Object[] 数组,用于存储元素,数组的大小会根据元素的增加动态扩容。
  • 认初始容量为10,可以通过构造函数指定初始容量(ArrayList<Integer> list = new ArrayList<>(20); // 初始容量为 20)
  • 动态扩容,当元素数量超过当前数组容量时,ArrayList 会自动扩容。
  • 间复杂度:
    • 随机访问:O(1)(通过索引直接访问)。
    • 插入/删除:
      • 在末尾插入/删除:O(1)
      • 在中间或开头插入/删除:O(n)(需要移动元素)。
    • 搜索:O(n)(需要遍历数组)。

2.2.2、扩容机制详解

1、机制
  • 新容量 = 旧容量 + 旧容量 / 2(即扩容为原来的 1.5 倍)。
  • 如果扩容后的容量仍然不足,则直接使用所需的最小容量。
  • 将旧数组中的元素复制到新数组中。
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍扩容
2、详解
  • 触发条件:当调用 add() 方法添加元素时,如果当前元素数量等于数组容量,就会触发扩容。

扩容过程

  1. 检查是否需要扩容:
if (size + 1 > elementData.length) {
    grow(size + 1); // 扩容
}
  1. 计算新容量:
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5 倍扩容
  1. 如果新容量仍然不足,则直接使用所需的最小容量:
if (newCapacity - minCapacity < 0) {
    newCapacity = minCapacity;
}
  1. 如果新容量超过最大数组大小(Integer.MAX_VALUE - 8),则抛出 OutOfMemoryError。
  2. 创建新数组并复制元素.

2.2.3、常用方法

  • add(E e):在末尾添加元素。
  • add(int index, E element):在指定位置插入元素。
  • remove(int index):删除指定位置的元素。
  • remove(Object o):删除指定元素(首次出现)。
  • get(int index):获取指定位置的元素。
  • set(int index, E element):修改指定位置的元素。
  • size():返回元素数量。
  • isEmpty():判断是否为空。
  • contains(Object o):判断是否包含指定元素。
  • clear():清空所有元素。

2.2.4、使用场景

1、适用场景
  • 频繁随机访问:ArrayList 基于数组实现,支持通过索引快速访问元素,适合需要频繁读取数据的场景。
  • 数据量较小或相对固定:如果数据量较小,ArrayList 的性能优于 LinkedList;当数据量相对固定,可以通过指定初始容量避免频繁扩容。
  • 末尾插入/删除操作:在末尾插入或删除元素时,ArrayList 的性能很高(O(1))。
2、不适用场景
  • 频繁在中间或开头插入/删除:在中间或开头插入/删除元素时,ArrayList 需要移动大量元素,性能较差(O(n))。此时更适合使用 LinkedList。
  • 数据量巨大且频繁扩容:如果数据量巨大且频繁扩容,ArrayList 的性能会受到影响。可以通过预先指定较大的初始容量来优化。

2.3、LinkedList详解

2.3.1、概述

LinkedList是 Java 中一个常用的集合类,它是List 接口的实现类,基于双向链表实现。与 ArrayList 不同,LinkedList在插入和删除操作上具有更高的效率,但在随机访问上性能较差。LinkedList 允许存储重复元素和 null 值。插入和删除操作时的间复杂度为 O(1)。随机访问的时间复杂度为 O(n)。并且是线程不安全的。

  • LinkedList 的底层是一个双向链表,每个节点(Node)包含三个部分:
    • 前驱节点(prev)
    • 当前元素(item)
    • 后继节点(next)
  • 节点定义:
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;
    }
}
  • 动态扩容
    • LinkedList 不需要预先分配内存空间,因此没有固定的初始容量。
    • 每次添加元素时,动态创建新节点并链接到链表中。
    • 由于链表的特性,LinkedList 不需要像 ArrayList 那样进行扩容操作。
  • 时间复杂度
    • 插入/删除:
      • 在开头或末尾插入/删除:O(1)
      • 在中间插入/删除:O(n)(需要遍历链表找到指定位置)。
    • 随机访问:O(n)(需要从头或尾遍历链表)。
    • 搜索:O(n)(需要遍历链表)。

2.3.2、常用方法

  • add(E e):在末尾添加元素。
  • addFirst(E e):在开头添加元素。
  • addLast(E e):在末尾添加元素。
  • add(int index, E element):在指定位置插入元素。
  • remove():删除并返回第一个元素。
  • removeFirst():删除并返回第一个元素。
  • removeLast():删除并返回最后一个元素。
  • remove(int index):删除指定位置的元素。
  • remove(Object o):删除指定元素(首次出现)。
  • get(int index):获取指定位置的元素。
  • getFirst():获取第一个元素。
  • getLast():获取最后一个元素。
  • set(int index, E element):修改指定位置的元素。
  • size():返回元素数量。
  • isEmpty():判断是否为空。
  • contains(Object o):判断是否包含指定元素。
  • clear():清空所有元素。

2.3.3、使用场景

1、适用场景
  • 频繁插入/删除操作:在开头或末尾插入/删除元素时,LinkedList 的性能很高(O(1)), 适合需要频繁修改数据的场景。
  • 实现队列或栈:LinkedList 实现了 Deque 接口,可以用作队列(FIFO)或栈(LIFO)。
// 作为队列使用
Queue<Integer> queue = new LinkedList<>();
queue.offer(1); // 入队
queue.poll();   // 出队

// 作为栈使用
Deque<Integer> stack = new LinkedList<>();
stack.push(1); // 入栈
stack.pop();   // 出栈
  • 数据量较大且需要动态增长:LinkedList 不需要预先分配内存空间,适合数据量较大且不确定的场景。
2、不适用场景
  • 频繁随机访问:LinkedList的随机访问性能较差(O(n))。如果需要频繁通过索引访问元素,更适合使用ArrayList
  • 内存占用敏感:LinkedList的每个节点需要额外的内存空间存储前驱和后继指针,内存占用较高。

2.3.4、LinkedList 与 ArrayList 的对比

特性ArrayListLinkedList
底层数据结构动态数组双向链表
随机访问性能O(1)O(n)
插入/删除性能末尾 O(1),中间 O(n)开头/末尾 O(1),中间 O(n)
内存占用较低(仅存储数据)较高(存储数据和指针)
适用场景频繁随机访问,数据量较小频繁插入/删除,数据量较大

2.4、Stack详解

2.4.1、概述

Stack 是 Java 中一个基于 Vector 实现的栈数据结构。栈是一种后进先出(LIFO, Last In First Out)的集合,常用于需要后进先出逻辑的场景。Stack 是线程安全的,所有方法都使用synchronized关键字修饰,但由于同步锁的开销,性能较低。现代Java开发中更推荐使用Deque接口的实现类(如 ArrayDeque)来替代 Stack。由于 Stack 继承自Vector,因此具有Vector的所有特性,包括动态扩容和线程安全。

  • 底层数据结构:Stack 的底层是一个Object[]数组,继承自Vector
  • 栈的操作(如pushpoppeek)都是基于数组实现的。
  • 初始容量,默认初始容量为 10(继承自 Vector),可以通过构造函数指定初始容量。
  • 动态扩容,Stack 的扩容机制与 Vector 相同
  • 时间复杂度
    • 入栈(push):O(1)(在数组末尾添加元素)。
    • 出栈(pop):O(1)(从数组末尾移除元素)。
    • 查看栈顶元素(peek):O(1)(访问数组末尾元素)。
    • 搜索元素(search):O(n)(需要遍历数组)。

2.4.2、常用方法

  • push(E item):将元素压入栈顶。
  • pop():移除并返回栈顶元素。
  • peek():返回栈顶元素,但不移除。
  • isEmpty():判断栈是否为空。
  • search(Object o):返回元素在栈中的位置(从栈顶开始计数,栈顶为 1)。
  • size():返回栈中元素的数量。
  • clear():清空栈。

2.4.3、使用场景

1、适用场景
  • 后进先出(LIFO)逻辑:栈适合需要后进先出逻辑的场景
    • 函数调用栈(方法调用和返回)。
    • 表达式求值(如括号匹配、逆波兰表达式)。
    • 撤销操作(如文本编辑器的撤销功能)。
  • 递归算法:递归算法可以使用栈来模拟调用栈。
  • 深度优先搜索(DFS):在图或树的深度优先搜索中,栈用于存储待访问的节点。
2、不适用场景
  • 需要先进先出(FIFO)逻辑:如果需要先进先出逻辑,应该使用队列(如 LinkedListArrayDeque)。
  • 频繁随机访问:栈的设计目标是后进先出,不适合需要频繁随机访问的场景。

2.5、Vector详解

2.5.1、概述

Vector 是 Java 中一个古老的集合类,它是 List 接口的实现类,基于动态数组实现。VectorArrayList 类似,但它是线程安全的。Vector允许存储重复元素和 null 值。支持随机访问(通过索引),访问时间复杂度为 O(1)。并且线程安全,所有方法都使用 synchronized 关键字修饰。由于同步锁的开销,性能较低,现代 Java 开发中更推荐使用 ArrayList Collections.synchronizedList或者CopyOnWriteArrayList

  • 底层数据结构:Vector 的底层是一个 Object[] 数组,用于存储元素。数组的大小会根据元素的增加动态扩容。
  • 初始容量:默认初始容量为 10。可以通过构造函数指定初始容量和扩容增量。
Vector<Integer> vector = new Vector<>(20); // 初始容量为 20
Vector<Integer> vector = new Vector<>(20, 10); // 初始容量为 20,扩容增量为 10
  • 动态扩容:当元素数量超过当前数组容量时,Vector 会自动扩容。
  • 时间复杂度
    • 随机访问:O(1)(通过索引直接访问)。
    • 插入/删除:
      • 在末尾插入/删除:O(1)
      • 在中间或开头插入/删除:O(n)(需要移动元素)。
    • 搜索:O(n)(需要遍历数组)。

2.5.2、扩容机制详解

1、机制
  • 如果指定了扩容增量(capacityIncrement),则新容量 = 旧容量 + 扩容增量。
  • 如果未指定扩容增量,则新容量 = 旧容量 * 2
  • 如果扩容后的容量仍然不足,则直接使用所需的最小容量。
  • 将旧数组中的元素复制到新数组中。
int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
2、详解
  • 扩容触发条件:当调用 add() 方法添加元素时,如果当前元素数量等于数组容量,就会触发扩容。

扩容过程

  • 检查是否需要扩容:
if (elementCount + 1 > elementData.length) {
    grow(elementCount + 1); // 扩容
}
  • 计算新容量:
int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
  • 如果新容量仍然不足,则直接使用所需的最小容量:
if (newCapacity - minCapacity < 0) {
    newCapacity = minCapacity;
}
  • 如果新容量超过最大数组大小(Integer.MAX_VALUE - 8),则抛出 OutOfMemoryError
  • 创建新数组并复制元素:

2.5.3、Vector 的常用方法

  • add(E e):在末尾添加元素。
  • add(int index, E element):在指定位置插入元素。
  • remove(int index):删除指定位置的元素。
  • remove(Object o):删除指定元素(首次出现)。
  • get(int index):获取指定位置的元素。
  • set(int index, E element):修改指定位置的元素。
  • size():返回元素数量。
  • isEmpty():判断是否为空。
  • contains(Object o):判断是否包含指定元素。
  • clear():清空所有元素。

2.5.4、使用场景

1、适用场景
  • 多线程环境:Vector 是线程安全的,适合在多线程环境下使用。如果不需要线程安全,建议使用 ArrayList。
  • 频繁随机访问:Vector 基于数组实现,支持通过索引快速访问元素,适合需要频繁读取数据的场景。
  • 数据量较小或相对固定:如果数据量较小,Vector 的性能可以接受,如果数据量相对固定,可以通过指定初始容量避免频繁扩容。
2、不适用场景
  • 单线程环境:在单线程环境下,Vector的性能不如ArrayList,因为所有方法都使用了 synchronized关键字。
  • 频繁在中间或开头插入/删除:在中间或开头插入/删除元素时,Vector需要移动大量元素,性能较差(O(n))。此时更适合使用LinkedList
  • 数据量巨大且频繁扩容:如果数据量巨大且频繁扩容,Vector的性能会受到影响。可以通过预先指定较大的初始容量来优化。

2.5.5、Vector和ArrayList对比

特性VectorArrayList
线程安全
性能较低(同步锁开销较高
扩容机制指定增量或双倍扩容1.5 倍扩容
适用场景多线程环境单线程环境

2.6、CopyOnWriteArrayList详解

2.6.1、概述

CopyOnWriteArrayListjava.util.concurrent 包中的一个线程安全集合类,也是 Java 中一个线程安全的 List 实现类,基于写时复制(Copy-On-Write)机制实现,每次修改操作(如添加、删除、修改)都会创建一个新的数组副本。 CopyOnWriteArrayList适合读多写少的场景,读操作性能极高,写操作性能较低,在高并发环境下提供了高效的读取性能。

  • 线程安全:CopyOnWriteArrayList 是线程安全的,所有写操作都通过加锁保证线程安全。读操作不需要加锁,因此性能极高。
  • 写时复制机制:每次写操作(如添加、删除、修改)都会创建一个新的数组副本,写操作完成后,将新数组替换旧数组,读操作始终访问当前数组,因此不需要加锁。
  • 扩容机制:CopyOnWriteArrayList 的扩容机制与 ArrayList 类似,每次写操作都会创建一个新的数组副本。新数组的容量根据实际需求动态调整。如果新数组容量不足,则扩容为当前容量的 1.5 倍。
  • 时间复杂度:
    • 读操作:O(1)(直接访问数组元素)。
    • 写操作:O(n)(需要复制整个数组)。
    • 搜索操作:O(n)(需要遍历数组)。

2.6.2、常用方法

  • add(E e):在列表末尾添加元素。
  • add(int index, E element):在指定位置插入元素。
  • remove(int index):删除指定位置的元素。
  • remove(Object o):删除指定元素(首次出现)。
  • get(int index):获取指定位置的元素。
  • set(int index, E element):修改指定位置的元素。
  • size():返回元素数量。
  • isEmpty():判断是否为空。
  • contains(Object o):判断是否包含指定元素。
  • clear():清空所有元素。

2.6.3、使用场景

1、适用场景
  • 读多写少
    • 缓存系统。
    • 配置管理。
    • 事件监听器列表。
  • 高并发读取:读操作不需要加锁,性能极高,适合高并发读取场景。
  • 数据一致性要求高:写操作会创建新的数组副本,读操作始终访问旧数组,因此可以保证数据的一致性。
2、不适用场景
  • 写多读少:写操作性能较低,适合写多读少的场景。
  • 数据量巨大:每次写操作都需要复制整个数组,如果数据量巨大,可能会导致内存占用过高。
  • 实时性要求高:写操作会创建新的数组副本,读操作可能无法立即看到最新的修改。

2.7、Collections.synchronizedList详解

2.7.1、概述

  • Collections.synchronizedListjava.util.Collections 类中的一个静态方法。它接收一个 List 对象作为参数,并返回一个线程安全的同步列表。所有对同步列表的操作都是线程安全的,因为方法内部使用了 synchronized 关键字。适用于需要在多线程环境下共享和修改 List 的场景。

2.7.2、底层实现

Collections.synchronizedList 并不直接实现 List 接口,而是通过包装现有的 List 实现类(如 ArrayList 或 LinkedList)来提供线程安全的功能。它内部维护了一个 mutex 对象(锁对象),所有方法都通过 synchronized(mutex) 实现同步。

2.7.3、扩容机制

Collections.synchronizedList 的扩容机制取决于被包装的 List 实现类。如果包装的是 ArrayList,则扩容机制与 ArrayList 相同(1.5 倍扩容)。如果包装的是LinkedList,则不需要扩容(基于链表实现)。

2.7.4、线程安全问题

  • 基本操作线程安全:Collections.synchronizedList保证了单个方法的线程安全。
synchronizedList.add(1); // 线程安全
synchronizedList.remove(0); // 线程安全
  • 复合操作非线程安全:如果需要执行复合操作(如遍历列表并修改),仍然需要手动加锁。
synchronized (synchronizedList) {
    for (Integer item : synchronizedList) {
        if (item == 1) {
            synchronizedList.remove(item);
        }
    }
}

2.7.5、使用场景

1、适合的场景
  • 多线程环境:当多个线程需要共享和修改同一个 List 时,可以使用 Collections.synchronizedList 来保证线程安全。
  • 简单的同步需求:如果只需要简单的线程安全功能,而不需要复杂的并发控制(如分段锁或读写锁),Collections.synchronizedList是一个简单易用的选择。
  • 兼容现有代码:如果现有代码已经使用了ArrayListLinkedList,可以通过 Collections.synchronizedList快速实现线程安全,而无需修改大量代码。
2、不适合的场景
  • 高并发场景:在高并发场景下,Collections.synchronizedList 的性能较差,因为所有操作都需要竞争同一把锁。此时更适合使用CopyOnWriteArrayListConcurrentLinkedQueue
  • 需要高性能读操作:如果读操作远多于写操作,Collections.synchronizedList 的性能不如CopyOnWriteArrayList
  • 需要复杂的并发控制:如果需要更细粒度的并发控制(如读写锁或分段锁),Collections.synchronizedList无法满足需求。

三、Set接口及其实现类

3.1、概念

SetJava集合框架中的一个核心接口,继承自Collection接口,但没有定义额外的方法。它表示一个不包含重复元素的集合,因此每个元素在Set中只能出现一次。Set 的实现类通过不同的底层数据结构来保证唯一性。

  • 特点
    • 唯一性:集合中不能包含相同的元素(通过 equals() 和 hashCode() 判断)。
    • 无序性:大多数 Set 实现类不保证元素的顺序(LinkedHashSet 和 TreeSet 除外)。
    • 允许 null 元素:大多数 Set 实现类允许存储 null 值(TreeSet 除外)。
  • 核心方法
    • add(E e):向集合中添加元素。
    • remove(Object o):从集合中删除元素。
    • contains(Object o):判断集合是否包含指定元素。
    • size():返回集合的大小。
    • iterator():返回集合的迭代器。
  • 主要实现类
    • 线程安全
      • CopyOnWriteArraySet:基于写时复制机制实现,线程安全,适合读多写少。
      • ConcurrentSkipListSet:基于并发跳表(ConcurrentSkipListMap的键集合)实现。
    • 非线程安全
      • HashSet:基于哈希表(HashMap 的键集合)实现,元素无序,查找速度快。
      • LinkedHashSet:基于哈希表 + 双向链表实现,元素按插入顺序排列。
      • TreeSet:基于红黑树(NavigableMap 的键集合)实现。元素有序,查找速度较慢。
      • EnumSet:基于位向量实现,专为枚举设计,性能极高。
  • Collections.synchronizedSet:提供对现有 Set 实现的线程安全包装。使用同步块来保证线程安全性。

3.2、HashSet详解

3.2.1、概述

HashSetJava中最常用的集合类之一,它是 Set 接口的实现类,基于哈希表(HashMap)实现。HashSet不允许存储重复元素,并且不保证元素的顺序。允许存储 null 值。线程不安全,如果需要线程安全,可以使用Collections.synchronizedSet ConcurrentHashMap.newKeySet()

  • 底层数据结构:HashSet的底层是一个HashMap,元素存储在HashMap的键中,值是一个固定的PRESENT对象。
private transient HashMap<E, Object> map;
private static final Object PRESENT = new Object();
  • 初始容量和负载因子:默认初始容量为 16,默认负载因子为 0.75,可以通过构造函数指定初始容量和负载因子。
  • 动态扩容:当元素数量超过当前容量与负载因子的乘积时,HashSet 会自动扩容。
  • 间复杂度
    • 添加元素:O(1)(平均情况下)
    • 删除元素:O(1)(平均情况下)
    • 查找元素:O(1)(平均情况下)
    • 遍历元素:O(n)

3.2.2、扩容机制详解

  • 扩容触发条件:当元素数量超过 当前容量 * 负载因子时,HashSet 会自动扩容。

扩容过程

  • 计算新容量: 新容量 = 旧容量 * 2
int newCapacity = oldCapacity << 1; 
  • 创建新哈希表:
Node<K,V>[] newTable = new Node[newCapacity];
  • 将旧哈希表中的元素重新哈希到新哈希表中:
for (Node<K,V> e : oldTable) {
    while (e != null) {
        Node<K,V> next = e.next;
        int hash = hash(e.key);
        int i = indexFor(hash, newCapacity);
        e.next = newTable[i];
        newTable[i] = e;
        e = next;
    }
}

3.2.3、常用方法

  • add(E e):添加元素。
  • remove(Object o):删除指定元素。
  • contains(Object o):判断是否包含指定元素。
  • size():返回元素数量。
  • isEmpty():判断是否为空。
  • clear():清空所有元素。

3.2.4、使用场景

1、适用场景
  • 去重:HashSet不允许存储重复元素,适合需要去重的场景。
  • 快速查找:HashSet基于哈希表实现,查找性能极高(O(1)),适合需要频繁查找元素的场景。
  • 数据量较大:HashSet的扩容机制和哈希表特性使其适合存储大量数据。
2、不适用场景
  • 需要有序集合:HashSet不保证元素的顺序,如果需要有序集合,可以使用 LinkedHashSetTreeSet
  • 频繁插入和删除:如果频繁插入和删除元素,可能会导致哈希冲突和扩容,影响性能。
  • 内存占用敏感:HashSet的底层是HashMap,需要额外的内存存储键值对,内存占用较高。

3.3、LinkedHashSet详解

3.3.1、概述

LinkedHashSet 是 Java 中一个基于链表和哈希表实现的集合类,它是HashSet 的子类,同时实现了Set接口。LinkedHashSet不仅具有HashSet的去重和快速查找特性,还保留了元素的插入顺序。LinkedHashSet不允许存储重复元素,且允许存储 null 值。它保留了元素的插入顺序,即遍历顺序与插入顺序一致。LinkedHashSet线程不安全,需要线程安全,可以使用Collections.synchronizedSet

  • 底层数据结构:LinkedHashSet 的底层是一个 LinkedHashMap,结合了哈希表和双向链表的特性,哈希表用于快速查找和去重,双向链表用于维护元素的插入顺序。-- - - 始容量和负载因子:默认初始容量为 16,默认负载因子为 0.75,可以通过构造函数指定初始容量和负载因子。
  • 动态扩容:当元素数量超过当前容量与负载因子的乘积时,LinkedHashSet 会自动扩容。
  • 扩容过程
    • 计算新容量:新容量 = 旧容量 * 2
    • 创建新哈希表。
    • 将旧哈希表中的元素重新哈希到新哈希表中,并维护双向链表的顺序。
  • 时间复杂度:
    • 添加元素:O(1)(平均情况下)。
    • 删除元素:O(1)(平均情况下)。
    • 查找元素:O(1)(平均情况下)。
    • 遍历元素:O(n)。

3.3.2、常用方法

  • add(E e):添加元素
  • remove(Object o):删除指定元素。
  • contains(Object o):判断是否包含指定元素。
  • size():返回元素数量。
  • isEmpty():判断是否为空。
  • clear():清空所有元素。

3.3.3、使用场景

1、适用场景
  • 去重且保留插入顺序:LinkedHashSet不允许存储重复元素,并且保留了元素的插入顺序。
  • 需要快速查找和有序遍历: 基于哈希表实现,查找性能极高(O(1)),同时保留了插入顺序。
  • 缓存系统:适合需要去重且保留访问顺序的缓存系统。
2、不适合的场景
  • 频繁插入和删除:如果频繁插入和删除元素,可能会导致哈希冲突和扩容,影响性能。
  • 内存占用敏感:LinkedHashSet的底层是LinkedHashMap,需要额外的内存存储链表节点,内存占用较高。

3.4、TreeSet详解

3.4.1、概述

TreeSet是 Java 中一个基于红黑树(Red-Black Tree,一种自平衡二叉搜索树) 实现的集合类,它是 SortedSet接口的实现类。TreeSet不允许存储重复元素,并且元素按照自然顺序或自定义顺序排序。TreeSet 不允许存储 null值(除非使用自定义比较器),并且线程不安全,如果需要线程安全,可以使用 Collections.synchronizedSortedSet

  • 底层数据结构:TreeSet的底层是一个TreeMap,元素存储在 TreeMap 的键中,值是一个固定的PRESENT对象。
  • 元素按照自然顺序(Comparable 接口)或自定义顺序(Comparator 接口)排序。
  • 动态扩容:TreeSet 基于红黑树实现,不需要预先分配内存空间,因此没有固定的初始容量。每次添加元素时,动态调整红黑树的结构以保持平衡。
  • 时间复杂度
    • 添加元素:O(log n)。
    • 删除元素:O(log n)。
    • 查找元素:O(log n)。
    • 遍历元素:O(n)。

3.4.2、常用方法

  • add(E e):添加元素。
  • remove(Object o):删除指定元素。
  • set.remove(1); // 删除元素 1
  • contains(Object o):判断是否包含指定元素。
  • subSet(E from, E to):返回从 from(包含)到 to(不包含)的子集。
  • headSet(E to):返回小于 to 的子集。
  • tailSet(E from):返回大于等于 from 的子集。
  • size():返回元素数量。
  • isEmpty():判断是否为空。
  • clear():清空所有元素。

3.4.3、使用场景

1、适用场景
  • 需要有序集合:TreeSet 保证元素的有序性,适合需要排序的场景。
  • 范围查询:TreeSet 提供了高效的范围查询方法(如 subSet、headSet、tailSet)。
  • 去重且排序:TreeSet 不允许存储重复元素,并且元素有序,适合需要去重和排序的场景。
2、不适用场景
  • 频繁插入和删除:红黑树的平衡调整需要额外开销,频繁插入和删除操作性能较低。
  • 内存占用敏感:红黑树需要额外的内存存储节点和指针,内存占用较高。
  • 不需要排序:如果不需要排序,使用HashSetLinkedHashSet性能更高。

3.5、CopyOnWriteArraySet详解

3.5.1、概述

CopyOnWriteArraySet是 Java 中一个线程安全的 Set 实现类,基于写时复制(Copy-On-Write)机制实现。它是 CopyOnWriteArrayList 的包装类,适合读多写少的场景,在高并发环境下提供了高效的读取性能。线程安全,所有写操作都通过加锁保证线程安全,读操作不需要加锁。

  • 线程安全:CopyOnWriteArraySet是线程安全的,所有写操作都通过加锁保证线程安全。读操作不需要加锁,因此性能极高。
  • 写时复制机制:每次写操作(如添加、删除、修改)都会创建一个新的数组副本。写操作完成后,将新数组替换旧数组。读操作始终访问当前数组,因此不需要加锁。
  • 初始容量:默认初始容量为 0。可以通过构造函数指定初始容量。
  • 扩容机制:CopyOnWriteArraySet的扩容机制与CopyOnWriteArrayList相同,每次写操作都会创建一个新的数组副本。新数组的容量根据实际需求动态调整。如果新数组容量不足,则扩容为当前容量的 1.5 倍。
  • 时间复杂度
    • 读操作:O(1)(直接访问数组元素)。
    • 写操作:O(n)(需要复制整个数组)。
    • 搜索操作:O(n)(需要遍历数组)。

3.5.2、常用方法

  • add(E e):添加元素。
  • remove(Object o):删除指定元素。
  • contains(Object o):判断是否包含指定元素。
  • size():返回元素数量。
  • isEmpty():判断是否为空。
  • clear():清空所有元素。

3.5.3、使用场景

1、适用场景
  • 读多写少:
    • 缓存系统。
    • 配置管理。
    • 事件监听器列表。
  • 高并发读取:读操作不需要加锁,性能极高,适合高并发读取场景。
  • 数据一致性要求高:写操作会创建新的数组副本,读操作始终访问旧数组,因此可以保证数据的一致性。
2、不适用场景
  • 写多读少:写操作性能较低,适合写多读少的场景。
  • 数据量巨大每次写操作都需要复制整个数组,如果数据量巨大,可能会导致内存占用过高。
  • 实时性要求高:写操作会创建新的数组副本,读操作可能无法立即看到最新的修改。

3.6、ConcurrentSkipListSet 详解

3.6.1、概述

ConcurrentSkipListSet 是 Java 中一个线程安全的 Set 实现类,基于**跳表(Skip List)**实现。它是 SortedSet 接口的实现类,支持元素的有序性和高效的并发操作。ConcurrentSkipListSetjava.util.concurrent包中的一个线程安全集合类。不允许存储重复元素,且不允许存储 null 值。线程安全,支持高并发操作。

  • 底层数据结构:ConcurrentSkipListSet的底层是一个ConcurrentSkipListMap,元素存储在ConcurrentSkipListMap的键中,值是一个固定的PRESENT对象。
  • 排序机制:元素按照自然顺序(Comparable接口)或自定义顺序(Comparator 接口)排序。
  • 动态扩容:ConcurrentSkipListSet 基于跳表实现,不需要预先分配内存空间,因此没有固定的初始容量。每次添加元素时,动态调整跳表的结构以保持平衡。
  • 时间复杂度
    • 添加元素:O(log n)。
    • 删除元素:O(log n)。
    • 查找元素:O(log n)。
    • 遍历元素:O(n)。

3.6.2、跳表详解

跳表(Skip List)是一种概率性数据结构,允许快速查找、插入和删除操作,平均时间复杂度为O(log n)。它通过多层链表实现,每层都是前一层的子集,顶层链表元素最少,底层包含所有元素。

  • 多层链表:
    • 跳表由多层链表组成,底层包含所有元素,每上一层元素数量减半。
    • 每个节点有多个指针,指向同一层的下一个节点和下一层的对应节点。
  • 随机层数: 插入新节点时,通过随机算法决定其层数,确保每层元素数量大致减半,维持平衡。
  • 查找操作:从顶层开始查找,若当前节点值小于目标值,则向右移动;否则,向下移动,直到找到目标或确定不存在。
  • 插入操作:先查找插入位置,然后随机生成层数,在各层链表中插入新节点。
  • 删除操作:查找目标节点,从各层链表中移除。

3.6.3、常用方法

  • add(E e):添加元素。
  • remove(Object o):删除指定元素。
  • contains(Object o):判断是否包含指定元素。
  • subSet(E from, E to):返回从 from(包含)到 to(不包含)的子集。
  • headSet(E to):返回小于 to 的子集。
  • tailSet(E from):返回大于等于 from 的子集。
  • size():返回元素数量。
  • isEmpty():判断是否为空。
  • clear():清空所有元素。

3.6.4、使用场景

1、适用场景
  • 需要有序集合:ConcurrentSkipListSet保证元素的有序性,适合需要排序的场景。
  • 高并发环境:ConcurrentSkipListSet是线程安全的,适合高并发环境下的有序集合操作。
  • 范围查询:ConcurrentSkipListSet提供了高效的范围查询方法(如subSetheadSettailSet)。
  • 去重且排序:ConcurrentSkipListSet不允许存储重复元素,并且元素有序,适合需要去重和排序的场景。
2、不适用场景
  • 内存占用敏感:跳表需要额外的内存存储索引节点,内存占用较高。
  • 不需要排序:如果不需要排序,使用ConcurrentHashMap.newKeySet()性能更高。

3.7、EnumSet详解

3.7.1、概述

EnumSet是 Java 中一个专门用于枚举类型的高效集合类,它是Set接口的实现类。EnumSet基于位向量(bit vector) 实现,性能极高,且内存占用非常低。 EnumSet不允许存储null值,且只能存储同一枚举类型的元素。基于位向量(bit vector)实现,性能极高,内存占用非常低。线程不安全,如果需要线程安全,可以使用 Collections.synchronizedSet

  • 底层数据结构:EnumSet的底层是一个位向量(bit vector),每个枚举常量对应位向量中的一个位。
  • 初始容量:EnumSet的容量固定为枚举类型的常量数量。不需要动态扩容,因为枚举类型的常量数量是固定的。
  • 动态扩容:EnumSet不需要动态扩容,因为枚举类型的常量数量是固定的。
  • 时间复杂度
    • 添加元素:O(1)
    • 删除元素:O(1)
    • 查找元素:O(1)
    • 遍历元素:O(n)

3.7.2、位向量详解

位向量详解点击查看
位向量(Bit Vector)是一种基于二进制位(bit)的数据结构,用于高效地表示和操作一组布尔值或状态。每个位(bit)可以表示一个状态(0 或 1),常用于集合操作、压缩存储和快速查询等场景。

1、概念
  • 二进制位表示:位向量是一个二进制数组,每个位(bit)表示一个状态。例如,一个 8 位的位向量可以表示为:01011010,其中每一位可以是 0 或 1。
  • 索引与位的关系:每个索引(index)对应位向量的一个位。例如,索引 0 对应第 0 位,索引 1 对应第 1 位,依此类推。
  • 位操作:位向量支持高效的位操作,如按位与(&)按位或(|)按位异或(^)按位取反(~)等。这些操作可以用于实现集合的交集、并集、差集等。
  • 存储方式:位向量通常使用整数类型(如intlong)来存储。例如,一个long类型可以存储 64 位。
4、EnumSet 中的应用
  • EnumSet中,每个枚举常量对应位向量的一个位。如果该位为 1,表示枚举值存在于集合中;如果为 0,则表示不存在。
  • 枚举的序号(ordinal):每个枚举常量都有一个唯一的序号(ordinal),从 0 开始递增。例如,对于枚举enum Color { RED, GREEN, BLUE }RED.ordinal() = 0GREEN.ordinal() = 1BLUE.ordinal() = 2

3.7.3、实现原理

  • 存储结构:EnumSet内部使用一个long类型的变量(64 位)来存储位向量。如果枚举常量的数量超过 64,则会使用多个long变量。每个枚举常量的ordinal值对应位向量的某一位。例如,RED.ordinal() = 0对应位向量的第 0 位。

  • 添加元素:添加枚举值时,将对应位设置为 1。例如,添加RED时,将位向量的第 0 位设置为 1。

  • 删除元素:删除枚举值时,将对应位设置为 0。例如,删除RED时,将位向量的第 0 位设置为 0。

  • 判断元素是否存在:检查对应位是否为 1。例如,检查RED是否存在时,检查位向量的第 0 位是否为 1。

  • 集合操作:

    • 并集:使用按位或(|)操作。
    • 交集:使用按位与(&)操作。
    • 差集:使用按位与和非(& ~)操作。

3.7.4、常用方法

  • EnumSet.of(E e1, E e2, ...):创建一个包含指定元素的EnumSet
  • EnumSet.allOf(Class<E> elementType):创建一个包含指定枚举类型所有常量的 EnumSet
  • EnumSet.noneOf(Class<E> elementType):创建一个空的 EnumSet
  • EnumSet.range(E from, E to):创建一个包含从fromto范围内所有常量的 EnumSet
  • add(E e):添加元素。
  • remove(Object o):删除指定元素。
  • contains(Object o):判断是否包含指定元素。
  • size():返回元素数量。
  • isEmpty():判断是否为空。
  • clear():清空所有元素。

3.7.5、使用场景

1、适用场景
  • 枚举类型集合:EnumSet专门用于枚举类型,适合存储同一枚举类型的元素。
  • 高效集合操作:EnumSet基于位向量实现,性能极高,适合需要高效集合操作的场景。
  • 内存占用敏感:EnumSet的内存占用非常低,适合内存占用敏感的场景。
2、不适用场景
  • 非枚举类型集合:EnumSet只能存储枚举类型的元素,不适合存储非枚举类型的元素。
  • 需要存储null值:EnumSet不允许存储null值。

3.8、Collections.synchronizedSet详解

3.8.1、概述

Collections.synchronizedSetJava提供的一个工具方法,用于将普通的Set转换为线程安全的同步集合。它通过包装现有的Set实现类(如HashSetTreeSet),并为其所有方法添加同步锁,从而保证线程安全,适用于需要在多线程环境下共享和修改 Set 的场景。

  • 线程安全:Collections.synchronizedSet 通过为所有方法添加同步锁(synchronized)来保证线程安全。
  • 底层实现:Collections.synchronizedSet并不直接实现 Set 接口,而是通过包装现有的 Set实现类(如HashSetTreeSet)来提供线程安全的功能。它内部维护了一个 mutex对象(锁对象),所有方法都通过synchronized(mutex)实现同步。
  • 性能:由于所有方法都使用了同步锁,Collections.synchronizedSet的性能较低。在高并发场景下,可能会成为性能瓶颈。

3.8.2、使用场景

1、适合场景
  • 多线程环境:当多个线程需要共享和修改同一个Set时,可以使用Collections.synchronizedSet来保证线程安全。
  • 简单的同步需求:如果只需要简单的线程安全功能,而不需要复杂的并发控制(如分段锁或读写锁),Collections.synchronizedSet是一个简单易用的选择。
  • 兼容现有代码:如果现有代码已经使用了HashSetTreeSet,可以通过Collections.synchronizedSet快速实现线程安全,而无需修改大量代码。
2、不适合的场景
  • 高并发场景:在高并发场景下,Collections.synchronizedSet的性能较差,因为所有操作都需要竞争同一把锁。此时更适合使用CopyOnWriteArraySetConcurrentHashMap.newKeySet()
  • 需要高性能读操作:如果读操作远多于写操作,Collections.synchronizedSet的性能不如CopyOnWriteArraySet
  • 需要复杂的并发控制:如果需要更细粒度的并发控制(如读写锁或分段锁),Collections.synchronizedSet无法满足需求。

四、Queue接口及其实现类

4.1、概念

QueueJava集合框架中的一个核心接口,表示一种先进先出(FIFO)或优先级排序的队列。Queue接口继承自Collection接口,并定义了一些队列特有的操作(如插入、删除、检查元素等)。

  • 特点
    • 先进先出(FIFO):元素按照插入顺序排列,最先插入的元素最先被移除。
    • 优先级队列:元素按照自然顺序或自定义比较器排序,优先级最高的元素最先被移除。
    • 不允许 null 元素:大多数 Queue 实现类不允许存储 null 值。
  • 核心方法
    • add(E e):向队列中添加元素(如果队列已满,抛出异常)。
    • offer(E e):向队列中添加元素(如果队列已满,返回 false)。
    • remove():移除并返回队列头部的元素(如果队列为空,抛出异常)。
    • poll():移除并返回队列头部的元素(如果队列为空,返回 null)。
    • element():返回队列头部的元素(如果队列为空,抛出异常)。
    • peek():返回队列头部的元素(如果队列为空,返回 null)。
  • 主要实现类
    • 线程安全
      • ConcurrentLinkedQueue:基于链表实现的无界线程安全队列。
      • BlockingQueue 接口:BlockingQueueQueue的子接口,表示一种支持阻塞操作的队列。以下是BlockingQueue的主要实现类:
        • ArrayBlockingQueue:基于数组实现的有界阻塞队列。
        • LinkedBlockingQueue:基于链表实现的可选有界阻塞队列。
        • PriorityBlockingQueue:基于堆实现的无界阻塞队列,元素按自然顺序或自定义比较器排序。
        • SynchronousQueue:一种不存储元素的阻塞队列,每个插入操作必须等待另一个线程的移除操作。
        • DelayQueue:基于优先级队列实现的无界阻塞队列,元素只有在延迟到期后才能被移除。
    • 非线程安全
      • LinkedList:基于双向链表实现,可以作为队列或双端队列(Deque)使用。
      • PriorityQueue:基于堆(通常是小顶堆)实现,元素按自然顺序或自定义比较器排序。
      • ArrayDeque:基于动态数组实现的双端队列,可以作为队列或栈使用。

4.2、PriorityQueue详解

4.2.1、概述

PriorityQueueJava中一个基于优先级堆(Priority Heap)实现的集合类,它是 Queue接口的实现类。PriorityQueue允许存储元素,并且元素按照自然顺序或自定义顺序排序,不允许存储 null 值。但它线程不安全,如果需要线程安全,可以使用PriorityBlockingQueue

  • 底层数据结构:PriorityQueue的底层是一个Object[]数组,用于存储元素。使用堆(Heap)数据结构实现,通常是一个最小堆(Min-Heap)或最大堆(Max-Heap)。
  • 初始容量:默认初始容量为 11。
  • 动态扩容:当元素数量超过当前容量时,PriorityQueue会自动扩容。
  • 扩容机制:
    • 如果当前容量小于64,则新容量 = 旧容量 * 2
    • 如果当前容量大于等于64,则新容量 = 旧容量 * 1.5
    • 将旧数组中的元素复制到新数组中。
  • 时间复杂度
    • 添加元素:O(log n)
    • 删除元素:O(log n)
    • 查找元素:O(1)(获取堆顶元素)
    • 遍历元素:O(n)

4.2.2、常用方法

  • add(E e):添加元素。
  • poll():删除并返回堆顶元素。
  • peek():返回堆顶元素,但不删除。
  • size():返回元素数量。
  • isEmpty():判断是否为空。
  • clear():清空所有元素。

4.2.3、使用场景

1、适用场景
  • 优先级任务调度:PriorityQueue 适合用于优先级任务调度,例如任务队列中高优先级任务优先执行。
  • Top K问题:适合解决 Top K 问题,例如从大量数据中找出前 K 个最大或最小的元素。
  • 排序需求:适合需要动态排序的场景,例如实时数据流中的排序。
2、不适用场景
  • 频繁插入和删除:如果频繁插入和删除元素,可能会导致堆的调整开销较大,影响性能。
  • 内存占用敏感:PriorityQueue需要额外的内存存储堆结构,内存占用较高。

4.3、ArrayDeque详解

4.3.1、概述

ArrayDequeJava中一个基于动态数组实现的双端队列(Deque),它是Deque接口的实现类。ArrayDeque支持高效的插入、删除和查找操作,适合用作栈或队列。

ArrayDeque是基于动态数组实现,它允许在队列的两端插入和删除元素,支持栈(LIFO)和队列(FIFO)的操作,不允许存储null值。但它线程不安全,如果需要线程安全,可以使用 Collections.synchronizedDeque

  • 底层数据结构:ArrayDeque的底层是一个Object[]数组,用于存储元素。使用两个指针(headtail)来标记队列的头部和尾部。
  • 初始容量:默认初始容量为 16。
  • 动态扩容:当元素数量超过当前容量时,ArrayDeque会自动扩容。
  • 扩容机制:
    • 新容量 = 旧容量 * 2。
    • 将旧数组中的元素复制到新数组中。
  • 时间复杂度
    • 添加元素:O(1)(平均情况下)。
    • 删除元素:O(1)(平均情况下)。
    • 查找元素:O(1)(获取头部或尾部元素)。
    • 遍历元素:O(n)

4.3.2、常用方法

  • add(E e):在尾部添加元素。
  • addFirst(E e):在头部添加元素。
  • addLast(E e):在尾部添加元素。
  • remove():删除并返回头部元素。
  • removeFirst():删除并返回头部元素。
  • removeLast():删除并返回尾部元素。
  • peek():返回头部元素,但不删除。
  • peekFirst():返回头部元素,但不删除。
  • peekLast():返回尾部元素,但不删除。
  • size():返回元素数量。
  • isEmpty():判断是否为空。
  • clear():清空所有元素。

4.3.3、使用场景

1、适用场景
  • 栈(LIFO):ArrayDeque可以用作栈,支持pushpop操作。
  • 队列(FIFO):ArrayDeque可以用作队列,支持offerpoll操作。
  • 双端队列:ArrayDeque支持在队列的两端插入和删除元素。
  • 缓冲区管理:用于临时存储数据,支持高效的两端操作。
  • 算法实现:如广度优先搜索(BFS)、任务调度等需要双端操作的场景。
2、不适用场景
  • 频繁插入和删除:如果频繁插入和删除元素,可能会导致数组扩容和复制,影响性能。
  • 内存占用敏感:ArrayDeque需要额外的内存存储数组,内存占用较高。

4.4、ConcurrentLinkedQueue详解

4.4.1、概述

ConcurrentLinkedQueueJava中一个基于链表实现的无界线程安全队列,属于 java.util.concurrent包。它采用非阻塞算法(CASCompare-And-Swap)实现高并发场景下的线程安全,适用于多线程环境下的生产者 - 消费者模型。

  • 核心特性
    • 无界队列:队列的容量理论上是无界的,元素可以无限添加(受内存限制)。底层基于单向链表实现,每个节点(Node)包含一个元素和指向下一个节点的指针。
    • 线程安全:通过 CAS 操作保证线程安全,无需显式加锁,适合高并发场景。支持多线程并发插入(入队)和删除(出队)操作。
    • 高性能:无锁设计避免了线程阻塞,吞吐量高。遵循先进先出(FIFO)原则。
    • 不支持阻塞操作:若队列为空时调用 poll(),直接返回 null,不会阻塞线程。
  • 底层数据结构
    • 链表结构:每个节点 Node 包含元素和 volatile 修饰的 next 指针。
      • 头尾指针:头指针(head)指向队列的第一个元素。尾指针(tail)指向队列的最后一个元素。头尾指针均为 volatile 修饰,保证多线程可见性。
  • 扩容机制:ConcurrentLinkedQueue 没有显式扩容机制。

4.4.2、线程安全原理

ConcurrentLinkedQueue通过 CAS 操作 和 volatile 变量 实现线程安全:

  • CAS(Compare-And-Swap)CAS是一种无锁原子操作,通过硬件指令(如 sun.misc.Unsafe)保证操作的原子性。
    • 入队(offer()):
      • 通过CAS更新尾节点(tail)的next指针,将新节点追加到链表尾部。
      • 如果多个线程同时修改tailCAS会确保只有一个线程成功,其他线程重试。
    • 出队(poll()):
      • 通过CAS更新头节点(head)的item字段为null,并移动头指针。
      • 如果多个线程同时出队,CAS保证只有一个线程成功获取元素。
  • volatile变量
    • headtail指针均用volatile修饰,确保多线程间的可见性。
    • 节点的itemnext字段也用volatile修饰,保证修改后对其他线程立即可见。

4.2.3、使用场景

1、适用场景
  • 高并发生产者 - 消费者模型:适用于多线程同时生产数据、消费数据的场景(如任务分发系统)。
  • 无阻塞任务队列:适合需要高吞吐量且不依赖阻塞操作的系统(如实时数据处理)。
  • 替代synchronized或锁的队列:在需要减少锁竞争的场景下,提供更高的并发性能。
2、不适用场景
  • 需要阻塞操作:若队列为空时消费者需要阻塞等待,应使用BlockingQueue(如 LinkedBlockingQueue)。
  • 有界队列需求:若需要限制队列容量以防止内存耗尽,应使用有界队列(如 ArrayBlockingQueue)。

4.5、BlockingQueue接口详解

4.5.1、概述

BlockingQueueJava并发包(java.util.concurrent)中定义的接口,代表一个线程安全的队列,支持阻塞操作。它主要用于生产者 - 消费者模型,允许线程在队列满或空时自动阻塞或等待,直到条件满足。

  • 线程安全:所有操作(入队、出队)均保证线程安全。
  • 阻塞支持:
    • 队列满时入队阻塞:当队列已满,put()方法会阻塞线程,直到队列有空位。
    • 队列空时出队阻塞:当队列为空,take()方法会阻塞线程,直到队列有元素。
  • 有界与无界队列:
    • 有界队列:需指定容量(如ArrayBlockingQueue)。
    • 无界队列:理论上容量无限(如LinkedBlockingQueue默认 Integer.MAX_VALUE)。
  • 公平性选项:部分实现支持公平锁,避免线程饥饿。

4.5.2、常用方法

  • put(E e):插入元素,若队列满则阻塞线程,直到有空位。
  • take(): 移除并返回头部元素,若队列空则阻塞线程,直到有元素。
  • offer(E e): 插入元素,成功返回 true,队列满时立即返回 false(不阻塞)。
  • poll(): 移除并返回头部元素,队列空时立即返回 null(不阻塞)。
  • offer(E e, long timeout, TimeUnit unit): 插入元素,若队列满则等待指定时间,超时返回 false。
  • poll(long timeout, TimeUnit unit): 移除并返回头部元素,若队列空则等待指定时间,超时返回 null。

4.6、ArrayBlockingQueue详解

4.6.1、概述

ArrayBlockingQueueJava并发包(java.util.concurrent)中基于数组实现的有界阻塞队列,支持线程安全的生产者 - 消费者模型

  • 核心特性
    • 有界队列:初始化时需指定固定容量,不可动态扩容。
    • 线程安全:通过ReentrantLockCondition实现线程安全。
    • 阻塞操作:
      • 队列满时入队阻塞:put()方法在队列满时阻塞线程,直到有空位。
      • 队列空时出队阻塞:take()方法在队列空时阻塞线程,直到有元素。
    • 公平性选项:可配置公平锁(默认非公平),避免线程饥饿。
      • 如果构造函数中指定fair = true,则线程将以 FIFO 的顺序获取锁,确保公平性。
      • 如果未指定或fair = false,则线程获取锁的顺序是非确定性的。
    • 先进先出(FIFO):元素按插入顺序处理。
  • 底层数据结构:
    • 基于数组:内部使用Object[] items存储元素。
    • 循环队列:通过两个指针putIndex(入队位置)和takeIndex(出队位置)实现循环数组。
  • 扩容机制:容量在初始化时固定,无法动态扩展。防止无限制的资源消耗,适用于需严格控制队列长度的场景(如连接池)。

4.6.2、线程安全原理

  • 锁与条件变量
    • 单锁机制
      • ArrayBlockingQueue 使用一个内部的 ReentrantLock 来保护对队列的访问。
      • 所有操作(入队、出队)共享一个 ReentrantLock,从而避免多个线程同时修改队列。
    • 条件变量
      • 定义了两个条件变量:
        • notEmpty:用于通知消费者线程队列中有可用元素,队列为空时,消费者线程在此等待。
        • notFull:用于通知生产者线程队列中有可用空间,队列满时,生产者线程在此等待。
      • 当队列为空时,消费者线程会被挂起,直到生产者线程插入新元素并唤醒等待的消费者线程。
      • 当队列已满时,生产者线程会被挂起,直到消费者线程移除元素并唤醒等待的生产者线程。
  • 入队操作(put())
    • 流程:
      • 获取锁。
      • 若队列满,调用notFull.await()阻塞生产者线程。
      • 插入元素到putIndex 位置,更新putIndex(循环递增)。
      • 唤醒等待在notEmpty上的消费者线程。
      • 释放锁。
    • 代码示例
public void put(E e) throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            notFull.await(); // 队列满时阻塞
        enqueue(e); // 插入元素
    } finally {
        lock.unlock();
    }
}
private void enqueue(E x) {
    items[putIndex] = x;
    putIndex = (putIndex + 1) % items.length; // 循环指针
    count++;
    notEmpty.signal(); // 唤醒消费者
}
  • 出队操作(take())
    • 流程:
      • 获取锁。
      • 若队列空,调用notEmpty.await()阻塞消费者线程。
      • takeIndex位置取出元素,更新takeIndex(循环递增)。
      • 唤醒等待在 notFull 上的生产者线程。
      • 释放锁。
    • 代码
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await(); // 队列空时阻塞
        return dequeue(); // 取出元素
    } finally {
        lock.unlock();
    }
}
private E dequeue() {
    E x = (E) items[takeIndex];
    items[takeIndex] = null; // 帮助 GC
    takeIndex = (takeIndex + 1) % items.length; // 循环指针
    count--;
    notFull.signal(); // 唤醒生产者
    return x;
}

4.6.3、常用方法

  • put(E e):插入元素,队列满时阻塞线程。
  • take():移除并返回头部元素,队列空时阻塞线程。
  • offer(E e):插入元素,队列满时立即返回false
  • poll():移除并返回头部元素,队列空时立即返回null
  • peek():返回头部元素但不移除,队列空时返回null
  • remainingCapacity():返回队列剩余容量。

4.6.4、适用场景

  • 固定资源池:如数据库连接池,限制最大连接数。
  • 生产者-消费者模型:控制生产速率,防止内存溢出。
  • 流量削峰:在突发流量下缓冲任务,避免系统过载。

4.7、LinkedBlockingQueue详解

4.7.1、概述

LinkedBlockingQueueJava并发包(java.util.concurrent)中基于链表实现的可选有界阻塞队列,支持高并发的生产者-消费者模型

  • 特性
    • 可选有界队列:初始化时可指定容量,若未指定则默认为Integer.MAX_VALUE(接近无界)。
    • 线程安全:通过分离锁(入队锁和出队锁)实现高并发性能。
    • 阻塞操作:
      • 队列满时入队阻塞:put()方法在队列满时阻塞线程,直到有空位。
      • 队列空时出队阻塞:take()方法在队列空时阻塞线程,直到有元素。
    • 先进先出(FIFO):元素按插入顺序处理。
    • 链表结构:基于单向链表实现,动态扩展节点。
  • 底层数据结构
    • 链表节点:每个节点包含元素和指向下一个节点的指针。
    • 头尾指针
      • head:指向链表的头部(最早插入的元素)。
      • last:指向链表的尾部(最新插入的元素)。
    • 原子计数器:AtomicInteger记录当前元素数量,保证线程安全
  • 扩容机制
    • 容量固定:初始化时指定容量,队列满后无法动态扩容。
    • 默认无界:若未指定容量(即capacity = Integer.MAX_VALUE),理论上可无限添加元素(受内存限制)。

4.7.2、线程安全原理

  • 分离锁设计
    • 入队锁(putLock):控制插入操作(put()offer()),用于保护向队列尾部插入元素的操作。
    • 出队锁(takeLock):控制移除操作(take()poll()),用于保护从队列头部移除元素的操作。
    • 分离锁优势:入队和出队操作互不阻塞,显著减少锁竞争。
  • 条件变量
    • 定义了两个条件变量:
      • notFull:队列满时,生产者线程在此等待(关联putLock)。
      • notEmpty:队列空时,消费者线程在此等待(关联takeLock)。
    • 当队列为空时,消费者线程会被挂起,直到生产者线程插入新元素并唤醒等待的消费者线程。
    • 当队列已满时,生产者线程会被挂起,直到消费者线程移除元素并唤醒等待的生产者线程。
  • 入队操作(put()
public void put(E e) throws InterruptedException {
    Node<E> node = new Node<>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();
    try {
        while (count.get() == capacity) {
            notFull.await(); // 队列满时阻塞
        }
        enqueue(node); // 插入链表尾部
        int c = count.getAndIncrement();
        if (c + 1 < capacity) {
            notFull.signal(); // 唤醒其他生产者
        }
    } finally {
        putLock.unlock();
    }
    if (c == 0) {
        signalNotEmpty(); // 唤醒消费者
    }
}

private void enqueue(Node<E> node) {
    last = last.next = node;
}
  • 出队操作(take()
public E take() throws InterruptedException {
    final ReentrantLock takeLock = this.takeLock;
    final AtomicInteger count = this.count;
    takeLock.lockInterruptibly();
    try {
        while (count.get() == 0) {
            notEmpty.await(); // 队列空时阻塞
        }
        E x = dequeue(); // 移除链表头部
        int c = count.getAndDecrement();
        if (c > 1) {
            notEmpty.signal(); // 唤醒其他消费者
        }
        return x;
    } finally {
        takeLock.unlock();
    }
}

private E dequeue() {
    Node<E> h = head;
    Node<E> first = h.next;
    h.next = h; // 断开原头节点,帮助 GC
    head = first;
    E x = first.item;
    first.item = null; // 置空以帮助 GC
    return x;
}

4.7.3、常用方法

  • put(E e):插入元素,队列满时阻塞线程。
  • take():移除并返回头部元素,队列空时阻塞线程。
  • offer(E e):插入元素,队列满时立即返回false
  • poll():移除并返回头部元素,队列空时立即返回null
  • peek():返回头部元素但不移除,队列空时返回null
  • remainingCapacity():返回队列剩余容量(无界队列始终返回Integer.MAX_VALUE)。

4.7.4、使用场景

1、适用场景
  • 高并发生产者-消费者模型:分离锁设计支持高吞吐量,适合多线程频繁插入和移除操作。
  • 任务缓冲池:适用于需要缓冲突发流量的系统(如消息队列)。
  • 无界或大容量队列:默认无界,适合任务量不可预测的场景(如日志处理)。
2、不适用场景
  • 严格内存控制:默认无界队列可能导致内存耗尽(OOM)。替代方案:使用有界队列(如 ArrayBlockingQueue)。
  • 快速随机访问:链表结构访问中间元素的时间复杂度为 O(n),不适合需要随机访问的场景。
  • 低延迟要求:动态创建节点和GC压力可能影响实时性要求高的系统。

4.8、PriorityBlockingQueue详解

4.8.1、概述

PriorityBlockingQueueJava并发包(java.util.concurrent)中基于优先级堆实现的无界阻塞队列,支持线程安全的生产者 - 消费者模型,元素按优先级排序。

  • 核心特性
    • 无界队列:理论上容量无限(实际受内存限制),动态扩容。
    • 线程安全:通过ReentrantLockCondition实现线程安全。
    • 优先级排序:
      • 元素按自然顺序(Comparable)或自定义Comparator排序。
      • 堆结构:底层基于数组实现的最小堆(默认)或最大堆(通过Comparator控制)。
    • 阻塞操作:
      • 队列空时出队阻塞:take()方法在队列空时阻塞线程,直到有元素。
      • 无入队阻塞:由于无界,put()offer()永远不会阻塞。
  • 底层数据结构
    • 堆(数组):元素存储在Object[] queue中,满足堆性质(父节点优先级 ≤ 子节点)。
    • 堆操作:
      • 插入元素:上浮(siftUp)调整堆。
      • 移除元素:下沉(siftDown)调整堆。
  • 动态扩容:
    • 扩容触发:当插入元素时,若当前数组已满,触发扩容。
    • 扩容策略:
      • 若当前容量 < 64,新容量 = 旧容量 * 2 + 2。
      • 若当前容量 ≥ 64,新容量 = 旧容量 * 1.5。

4.8.2、线程安全原理

  • 锁与条件变量
    • 单锁机制:所有操作(入队、出队)共享一个ReentrantLock
    • 条件变量:notEmpty用于队列空时阻塞消费者线程。
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
  • 入队操作(offer()
public boolean offer(E e) {
    if (e == null) throw new NullPointerException();
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        int n = size;
        if (n >= queue.length)
            grow(n + 1); // 动态扩容
        siftUp(n, e);    // 上浮调整堆
        size = n + 1;
        notEmpty.signal(); // 唤醒消费者
        return true;
    } finally {
        lock.unlock();
    }
}

3.3 出队操作(take()

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    E result;
    try {
        while (size == 0)
            notEmpty.await(); // 队列空时阻塞
        result = dequeue();   // 下沉调整堆
    } finally {
        lock.unlock();
    }
    return result;
}

4.8.3、常用方法

  • put(E e):插入元素(永不阻塞)。
  • take():移除并返回队首元素,队列空时阻塞线程。
  • offer(E e):插入元素,始终返回true(无界队列不会满)。
  • poll():移除并返回队首元素,队列空时返回 null
  • peek():返回队首元素但不移除,队列空时返回 null
  • remainingCapacity():始终返回Integer.MAX_VALUE(无界队列)。

4.8.4、使用场景

1、适用场景
  • 优先级任务调度:高优先级任务优先处理(如紧急告警系统)。
  • 延迟队列(结合 Delayed 接口):用于定时任务调度(如 DelayQueue 的实现)。
  • 高并发场景:由于其高效的锁机制,适合高并发环境下的队列操作。
2、不适用场景
  • 内存敏感场景:无界特性可能导致内存耗尽(OOM),需谨慎使用。替代方案使用有界队列(如 ArrayBlockingQueue)。
  • 高频次操作:插入和删除的时间复杂度为 O(log n),高频操作可能影响性能。
  • 严格顺序要求:同优先级元素的处理顺序不确定(堆不保证稳定性)。

4.9、SynchronousQueue详解

4.9.1、概述

SynchronousQueueJava并发包(java.util.concurrent)中一种特殊的阻塞队列,它不存储元素,而是直接在生产者和消费者之间传递数据。它并不是一个真正意义上的集合,因为它不存储元素。相反,它是一个用于线程间直接传递数据的同步机制。每个插入操作必须等待另一个线程对应的移除操作,反之亦然。

  • 核心特性
    • 无存储结构:队列本身不存储元素,每个插入操作必须等待一个对应的移除操作(一个线程往队列放数据的时候,必须等待另一个线程从队列中取走数据,反之亦然)。
    • 线程安全:通过CAS操作和锁实现线程安全。
    • 阻塞操作:
      • 生产者阻塞:put()方法在无消费者时阻塞,直到有消费者取走元素。
      • 消费者阻塞:take() 方法在无生产者时阻塞,直到有生产者插入元素。
    • 公平性选项:支持公平模式(FIFO)和非公平模式(默认)。
      • 公平模式:基于队列(FIFO)实现,保证等待时间最长的线程优先执行。
      • 非公平模式:基于栈(LIFO)实现,性能更高,但可能导致线程饥饿。
    • 零容量:队列容量始终为 0,元素直接传递。
  • 扩容机制:SynchronousQueue不存储元素,容量始终为 0,无需扩容。
  • 底层数据结构:SynchronousQueue不维护任何内部存储结构,元素直接在生产者和消费者之间传递。

4.9.2、线程安全原理

  • CAS操作:
    • CAS(Compare-And-Swap),通过CAS 操作实现无锁线程安全,避免锁竞争。
    • 节点匹配:生产者和消费者通过CAS操作匹配节点,完成数据传递。
1、公平模式(TransferQueue
  • 队列结构:使用FIFO队列管理等待线程。
  • 匹配机制:
    • 生产者插入节点,等待消费者匹配。
    • 消费者移除节点,唤醒匹配的生产者。
static final class TransferQueue<E> extends Transferer<E> {
    volatile QNode head; // 队列头
    volatile QNode tail; // 队列尾
}
2、非公平模式(TransferStack
  • 栈结构:使用LIFO栈管理等待线程。
  • 匹配机制:
    • 生产者插入节点,等待消费者匹配。
    • 消费者移除节点,唤醒匹配的生产者。
static final class TransferStack<E> extends Transferer<E> {
    volatile SNode head; // 栈顶
}
3、入队操作(put()
public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    if (transferer.transfer(e, false, 0) == null) {
        Thread.interrupted();
        throw new InterruptedException();
    }
}
4、出队操作(take()
public E take() throws InterruptedException {
    E e = transferer.transfer(null, false, 0);
    if (e != null)
        return e;
    Thread.interrupted();
    throw new InterruptedException();
}

4.9.3、常用方法

  • put(E e):插入元素,若无消费者则阻塞线程。
  • take():移除并返回元素,若无生产者则阻塞线程。
  • offer(E e):插入元素,若无消费者则立即返回 false。
  • poll():移除并返回元素,若无生产者则立即返回 null。
  • peek():始终返回 null(无存储结构)。
  • remainingCapacity():始终返回 0(容量为 0)。

4.9.4、使用场景

1、适用场景
  • 直接传递任务:生产者必须等待消费者取走元素,适合任务直接传递的场景。
  • 线程池任务分配:用于Executors.newCachedThreadPool的任务队列,动态创建线程处理任务。newCachedThreadPool线程池的核心线程数是0,最大线程数是Integer的最大值,线程存活时间是60秒。如果你newCachedThreadPool线程池,提交的任务会被更快速的处理,因为每次提交任务,都会有一个空闲的线程等着处理任务。如果没有空闲的线程,也会立即创建一个线程处理任务。
  • 高吞吐量系统:无锁设计减少竞争,适合高并发场景。
2、不适用场景
  • 需要缓冲任务:无法存储元素,不适合需要缓冲任务的场景。
  • 替代方案:使用 LinkedBlockingQueue 或 ArrayBlockingQueue。
  • 低延迟要求:生产者和消费者必须严格同步,可能增加延迟。
  • 任务量波动大:若生产者或消费者数量不均衡,可能导致线程阻塞或资源浪费。

4.10、DelayQueue详解

4.10.1、概述

DelayQueue(延迟队列)是Java并发包(java.util.concurrent)中基于优先级堆实现的无界阻塞队列,用于存储实现了Delayed接口的元素。元素只有在其延迟时间到期后才能被取出。不允许存储null元素。

  • Delayed接口:
    • Delayed接口是java.util.concurrent包中的一个接口,它继承自Comparable接口。
    • Delayed接口要求实现类提供一个方法getDelay(TimeUnit unit),该方法返回当前元素还需延迟的时间。
  • 核心特性
    • 无界队列:理论上容量无限(实际受内存限制),动态扩容。
    • 延迟元素:元素必须实现 Delayed 接口,指定延迟时间。
    • 优先级排序:元素按延迟时间排序,最早到期的元素优先出队。
    • 线程安全:通过ReentrantLockCondition实现线程安全。
    • 阻塞操作:
      • 队列空时出队阻塞:take()方法在队列空时阻塞线程,直到有元素到期。
      • 无入队阻塞:由于无界,put()offer()永远不会阻塞。
  • 扩容机制
    • 扩容触发:当插入元素时,若当前数组已满,触发扩容。
    • 扩容策略:与PriorityQueue相同,容量扩展为原大小的 50%。
  • 底层数据结构
    • 优先级堆:元素存储在PriorityQueue中,按延迟时间排序。
    • 延迟接口:元素必须实现Delayed接口,定义getDelay()compareTo()方法。
public interface Delayed extends Comparable<Delayed> {
    long getDelay(TimeUnit unit); // 返回剩余延迟时间
}

4.10.2、线程安全原理

  • 锁与条件变量
    • 单锁机制:所有操作(入队、出队)共享一个ReentrantLock
    • 条件变量:available用于队列空时阻塞消费者线程。
private final transient ReentrantLock lock = new ReentrantLock();
private final Condition available = lock.newCondition();
1、入队操作(offer()
  • 在真正执行插入逻辑之前会先上一把可重入锁,确保线程安全,最后在执行完插入逻辑之后释放当前锁;
  • 将当前任务放入优先队列中,确保堆顶元素是最小的,即小顶堆;
  • 判断当前优先队列中堆顶的任务是否与当前插入任务相等,如果相等,就将leader设置为null并将Condition信号量设置为signal唤醒一个消费者。
public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        q.offer(e); // 插入优先级堆
        if (q.peek() == e) { // 如果新元素是最早到期的
            available.signal(); // 唤醒消费者
        }
        return true;
    } finally {
        lock.unlock();
    }
}
2、出队操作(take()
  • 首先还是在真正逻辑的前后加一把可重入锁和执行完逻辑后释放锁;
  • take方法中使用一个无限循环来获取到期的任务,如果当前没有到期的任务则一直自旋阻塞,直到有满足执行条件的任务为止;
  • 从优先队列中获取最小堆堆顶元素(peek方法不会删除元素),如果当前堆顶元素为空,就会进入阻塞状态;
  • 如果当前堆顶元素不为空,就会判断当前任务是否达到了执行时间,达到执行时间直接返回任务,反之阻塞当前线程,直到堆顶元素到期;
  • 如果leader不为空,表明已经有其他线程正在等待这个堆顶元素;
  • 如果leader为空,就将leader设置为当前线程,并将Condition信号量设置为delay纳秒阻塞时间;
  • leader线程被唤醒并获取到锁之后会将leader设置为空。
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        for (;;) {
            E first = q.peek(); // 获取堆顶元素
            if (first == null) {
                available.await(); // 队列空时阻塞
            } else {
                long delay = first.getDelay(NANOSECONDS);
                if (delay <= 0) {
                    return q.poll(); // 到期,移除元素
                }
                first = null; // GC
                if (leader != null) {
                    available.await(); // 其他线程正在等待
                } else {
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        available.awaitNanos(delay); // 等待延迟时间
                    } finally {
                        if (leader == thisThread) {
                            leader = null;
                        }
                    }
                }
            }
        }
    } finally {
        if (leader == null && q.peek() != null) {
            available.signal(); // 唤醒其他消费者
        }
        lock.unlock();
    }
}

4.10.3、延迟队列原理

1、原理
  • DelayQueue的泛型参数需要实现Delayed接口,Delayed接口继承了Comparable接口,DelayQueue内部使用非线程安全的优先队列(PriorityQueue),并使用Leader/Followers模式,它是一种常见的多线程同步模式,最小化不必要的等待时间。
  • 详细的过程查看4.10.2
2、Leader/Followers模式
  • Leader/Followers就是永远只有一个leader线程处于可消费状态,而其它的follower线程就只有在成为leader才能切换为可消费状态。从上面的offertake方法来看,每当当前任务不处于执行时间,就会将当前线程设置为leader便于阻塞持续监听,同时也防止其它线程参与不良的竞争。只要堆顶任务已经执行完毕,就会将当前与leader绑定的线程清空,然后开启下一轮leader选举。
  • Leader/Followers模式的基本原则就是,永远最多只有一个leader。而所有follower都在等待成为leader。线程池启动时会自动产生一个Leader负责等待网络IO事件,当有一个事件产生时,Leader线程首先通知一个Follower线程将其提拔为新的Leader,然后自己就去干活了,去处理这个网络事件,处理完毕后加入Follower线程等待队列,等待下次成为Leader。这种方法可以增强CPU高速缓存相似性,及消除动态内存分配和线程间的数据交换。
  • Leader/Followers的线程模型一共有FollowerCandidateLeader三个状态。
    • Follower
      • 线程处于等待状态,等待被唤醒或被选为候选者。
      • 当Leader线程释放领导权时,Follower线程有机会被选为新的候选者。
    • Candidate
      • 线程尝试获取领导权,可能会成功也可能失败。
        • 如果成功,线程将成为新的Leader
        • 如果失败,线程将回到Follower状态。
    • Leader
      • 线程拥有领导权,可以执行特定的任务,例如在DelayQueue中取出到期的元素。
      • Leader线程在完成任务或等待超时后,将释放领导权,并回到Follower状态。

4.10.4、常用方法

  • put(E e):插入元素(永不阻塞)。
  • take():移除并返回队首元素,队列空时阻塞线程。
  • offer(E e):插入元素,始终返回true(无界队列不会满)。
  • poll():移除并返回队首元素,队列空时返回null
  • peek():返回队首元素但不移除,队列空时返回null
  • remainingCapacity():始终返回Integer.MAX_VALUE(无界队列)。

4.10.5、使用场景

1、适用场景
  • 定时任务调度:用于实现定时任务调度系统(如 ScheduledThreadPoolExecutor)。
DelayQueue<DelayedTask> queue = new DelayQueue<>();
queue.put(new DelayedTask("Task1", 10, TimeUnit.SECONDS));
queue.put(new DelayedTask("Task2", 5, TimeUnit.SECONDS));
DelayedTask task = queue.take(); // 5秒后取出 Task2
  • 缓存过期清理:用于管理缓存中的过期条目。
DelayQueue<CacheEntry> cache = new DelayQueue<>();
cache.put(new CacheEntry("key1", "value1", 10, TimeUnit.MINUTES));
CacheEntry entry = cache.take(); // 10分钟后清理
  • 延迟消息队列:用于实现延迟消息传递(如消息队列系统)。
2、不适用场景
  • 内存敏感场景:无界特性可能导致内存耗尽(OOM),需谨慎使用。可使用有界队列(如ArrayBlockingQueue)替代。
  • 高频次操作:插入和删除的时间复杂度为O(log n),高频操作可能影响性能。
  • 严格顺序要求:同延迟时间的元素处理顺序不确定(堆不保证稳定性)。

五、Map接口及其实现类

5.1、概念

MapJava集合框架中的一个核心接口,表示键值对的集合。Map中的键不允许重复,每个键最多映射到一个值。

  • 特点
    • 键值对存储:每个元素包含一个键和一个值。
    • 键不允许重复:通过equals()hashCode()方法判断键是否重复。
    • 允许 null 键和 null 值:大多数 Map 实现类允许存储null键和null值(TreeMapConcurrentHashMap除外)。
  • 核心方法
    • put(K key, V value):添加键值对。
    • get(Object key):根据键获取值。
    • remove(Object key):删除键值对。
    • containsKey(Object key):判断是否包含指定键。
    • containsValue(Object value):判断是否包含指定值。
    • keySet():返回所有键的集合。
    • values():返回所有值的集合。
    • entrySet():返回所有键值对的集合。
  • 主要实现类
    • 线程安全
      • Hashtable:基于哈希表实现,键值对无序。(方法使用 synchronized修饰)
      • ConcurrentHashMap:基于分段锁(JDK 7)或 CAS 操作(JDK 8+)实现的高性能线程安全哈希表。
    • 非线程安全
      • HashMap:基于哈希表实现,键值对无序。
      • LinkedHashMap:基于哈希表和链表实现,键值对按插入顺序或访问顺序排列。
      • TreeMap:基于红黑树实现,键值对按自然顺序或自定义比较器排序。
      • WeakHashMap:基于哈希表实现,键是弱引用(WeakReference),当键不再被强引用时,键值对会被自动移除。
      • IdentityHashMap:基于哈希表实现,使用==而不是equals()方法比较键。
      • EnumMap:专为枚举类型设计的哈希表,性能极高。
  • Collections.synchronizedMap:通过包装非线程安全的 Map 实现线程安全。所有方法都使用 synchronized 修饰。

5.2、HashMap详解

5.2.1、概述

HashMapJava中最常用的集合类之一,它是Map接口的实现类,基于哈希表(Hash Table)实现。HashMap 允许存储键值对,并且键和值都可以为 null。HashMap 不保证元素的顺序,元素的存储顺序可能与插入顺序不同。并且它线程不安全,如果需要线程安全,可以使用 Collections.synchronizedMapConcurrentHashMap

  • 底层数据结构:HashMap内部是一个数组 + 链表/红黑树的组合数据结构:
    • 数组是核心,用于存储桶(Bucket)。
    • 每个桶中存放的是链表或红黑树,用于解决哈希冲突。
      • 哈希表用于快速查找和去重。
      • 双向链表用于维护元素的插入顺序或访问顺序。
  • 初始容量和负载因子:默认初始容量为 16,默认负载因子为 0.75。可以通过构造函数指定初始容量和负载因子。
  • 动态扩容:当元素数量超过当前容量与负载因子的乘积时,HashMap 会自动扩容。
  • 时间复杂度:
    • 添加元素:O(1)(平均情况下)。
    • 删除元素:O(1)(平均情况下)。
    • 查找元素:O(1)(平均情况下)。
    • 遍历元素:O(n)。

5.2.2、内部结构

  • 节点结构:HashMap 的节点由内部类 Node 表示
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;       // 哈希值
    final K key;          // 键
    V value;              // 值
    Node<K,V> next;       // 下一个节点(链表指针)
}
  • 红黑树节点:当链表转换为红黑树时,使用 TreeNode 类:
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // 父节点
    TreeNode<K,V> left;    // 左子节点
    TreeNode<K,V> right;   // 右子节点
    TreeNode<K,V> prev;    // 链表前驱节点
    boolean red;           // 是否为红色节点
}

5.2.3、扩容机制详解

  • 扩容触发条件:当元素数量超过当前容量与负载因子的乘积时,HashMap 会自动扩容。

扩容过程

  • 计算新容量:新容量 = 旧容量 * 2。
  • 建新哈希表。
  • 旧哈希表中的元素重新哈希到新哈希表中。

5.2.4、哈希冲突解决方式

1、概念
  • 哈希冲突:当多个键的哈希值映射到同一个桶(Bucket)时,会发生哈希冲突。
  • 哈希函数:HashMap 使用对象的hashCode() 方法生成哈希值,并通过位运算将哈希值映射到数组索引,index = (hash & (capacity - 1))其中 hash 是经过扰动处理后的哈希值,capacity 是数组容量。
2、解决方式
  • 链地址法:链表法是哈希冲突解决的最基本方法。它的核心思想是将哈希表的每个数组元素作为一个链表的头节点,所有哈希值相同的键值对都存储在同一个链表中
    • 数据结构:哈希表是一个数组,数组的每个元素是一个链表的头节点。每个链表节点存储键值对以及指向下一个节点的指针。
    • 操作流程
      • 插入键值对
        • 计算键的哈希值,找到对应的数组索引。
        • 如果该索引对应的链表为空,直接插入。
        • 如果链表不为空,遍历链表检查是否已存在相同的键:如果存在,更新值。如果不存在,将新节点插入链表头部或尾部。
      • 查找键值对
        • 计算键的哈希值,找到对应的数组索引。
        • 遍历链表,通过 equals()方法比较键是否相等。
        • 如果找到相等的键,返回对应的值;否则返回null
      • 删除键值对
        • 计算键的哈希值,找到对应的数组索引。
        • 遍历链表,找到对应的节点并删除。
    • 优点:
      • 实现简单,易于理解。
      • 适合小规模数据或哈希冲突较少的情况。
    • 缺点
      • 当链表过长时,查找、插入和删除的时间复杂度退化为 O(n)
      • 需要额外的空间存储链表指针。
  • 红黑树优化:是对链表法的优化。当链表长度超过 8 并且数组容量大于等于 64时,将链表转换为红黑树,以提高查找、插入和删除的性能。当链表长度减少到 6 以下时,红黑树会转换回链表。
    • 数据结构:哈希表是一个数组,数组的每个元素是一个链表或红黑树的根节点。当链表长度超过阈值(默认是 8)时,链表转换为红黑树。 当红黑树的节点数减少到一定阈值(默认是 6)时,红黑树退化为链表。

    • 操作流程

      • 插入键值对
        • 计算键的哈希值,找到对应的数组索引。
        • 如果该索引对应的链表为空,直接插入。
        • 如果链表不为空,遍历链表检查是否已存在相同的键:如果存在,更新值。如果不存在,将新节点插入链表尾部。如果链表长度超过阈值,将链表转换为红黑树。
      • 查找键值对
        • 计算键的哈希值,找到对应的数组索引。
        • 如果该索引对应的是链表,遍历链表查找。
        • 如果该索引对应的是红黑树,使用红黑树的查找算法。
      • 删除键值对
        • 计算键的哈希值,找到对应的数组索引。
        • 如果该索引对应的是链表,遍历链表删除。
        • 如果该索引对应的是红黑树,使用红黑树的删除算法。
        • 如果红黑树的节点数减少到阈值以下,将红黑树退化为链表。
    • 优点

      • 当哈希冲突严重时,红黑树的查找、插入和删除时间复杂度为O(logn),性能优于链表。
      • 适合大规模数据或哈希冲突较多的情况。
    • 缺点

      • 实现复杂,需要维护红黑树的平衡性。
      • 需要额外的空间存储红黑树的节点信息。

5.2.5、常用方法

  • put(K key, V value):添加键值对。
  • remove(Object key):删除指定键的键值对。
  • get(Object key):获取指定键的值。
  • size():返回键值对数量。
  • isEmpty():判断是否为空。
  • containsKey(Object key):判断是否包含指定键。
  • containsValue(Object value):判断是否包含指定值。
  • clear():清空所有键值对。

5.2.6、使用场景

1、适合的场景
  • 键值对存储:HashMap适合存储键值对,例如缓存系统、配置管理等。
  • 快速查找:HashMap基于哈希表实现,查找性能极高(O(1)),适合需要频繁查找键值对的场景。
  • 数据量较大:HashMap的扩容机制和哈希表特性使其适合存储大量数据。
2、不适合的场景
  • 需要有序集合:HashMap不保证元素的顺序,如果需要有序集合,可以使用 LinkedHashMapTreeMap
  • 频繁插入和删除:如果频繁插入和删除元素,可能会导致哈希冲突和扩容,影响性能。
  • 内存占用敏感:HashMap需要额外的内存存储键值对和链表/红黑树节点,内存占用较高。

5.3、LinkedHashMap详解

5.3.1、概述

LinkedHashMapJava 中一个基于链表和哈希表实现的集合类,它是 HashMap 的子类,同时实现了Map接口。LinkedHashMap 不仅具有HashMap的快速查找特性,还保留了元素的插入顺序或访问顺序。LinkedHashMap允许存储键值对,键和值都可以为 null。但它线程不安全,如果需要线程安全,可以使用Collections.synchronizedMap

  • 底层数据结构:LinkedHashMap 的底层是一个 HashMap,同时维护了一个双向链表。
    • 哈希表用于快速查找和去重。
    • 双向链表用于维护元素的插入顺序或访问顺序。
  • 初始容量和负载因子:默认初始容量为 16,默认负载因子为 0.75。
  • 动态扩容:当元素数量超过当前容量与负载因子的乘积时,LinkedHashMap 会自动扩容。
  • 扩容机制:完全继承自 HashMap
    • 新容量 = 旧容量 * 2。
    • 将旧哈希表中的元素重新哈希到新哈希表中。
    • 维护双向链表的顺序。
  • 哈希冲突解决方式:与 HashMap 完全一致(详情请看HashMap 5.2.5章节)。
    • 使用链地址法解决冲突,即将冲突的键值对存储在链表中。
    • 在 Java 8 中,当链表长度超过 8 并且数组容量大于等于 64 时,链表会转换为红黑树。
  • 时间复杂度
    • 添加元素:O(1)(平均情况下)。
    • 删除元素:O(1)(平均情况下)。
    • 查找元素:O(1)(平均情况下)。
    • 遍历元素:O(n)。

5.3.2、维护顺序

  • 插入顺序:默认情况下,LinkedHashMap 按照插入顺序维护元素。每次插入新元素时,都会将其添加到双向链表的尾部。
  • 访问顺序:在访问顺序模式下,每当访问一个键值对时,该键值对会被移动到双向链表的尾部。最近最少使用的键值对位于链表的头部,而最近使用的键值对位于链表的尾部。如果希望按照访问顺序维护元素,可以通过构造函数启用访问顺序模式:
LinkedHashMap<K, V> map = new LinkedHashMap<>(initialCapacity, loadFactor, true);//第三个参数 true 表示启用访问顺序模式。

5.3.3、内部结构

  • 节点结构:LinkedHashMap 的节点由内部类 LinkedHashMap.Entry 表示,扩展了 HashMap.Node:
static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after; // 双向链表指针

    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}
  • 双向链表:LinkedHashMap 使用两个字段维护双向链表的头尾节点:
transient LinkedHashMap.Entry<K,V> head; // 链表头
transient LinkedHashMap.Entry<K,V> tail; // 链表尾

5.3.4、常用方法

  • put(K key, V value):添加键值对。
  • remove(Object key):删除指定键的键值对。
  • get(Object key):获取指定键的值。
  • size():返回键值对数量。
  • isEmpty():判断是否为空。
  • containsKey(Object key):判断是否包含指定键。
  • containsValue(Object value):判断是否包含指定值。
  • clear():清空所有键值对。

5.3.5、使用的场景

1、适用场景
  • 需要保留插入顺序或访问顺序:LinkedHashMap 保留了元素的插入顺序或访问顺序,适合需要有序遍历的场景。
LinkedHashMap<Integer, String> map = new LinkedHashMap<>();
map.put(1, "apple");
map.put(2, "banana");
map.put(3, "cherry");
for (Map.Entry<Integer, String> entry : map.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue()); // 输出顺序:1: apple, 2: banana, 3: cherry
}
  • LRU 缓存:LinkedHashMap可以通过重写removeEldestEntry方法实现LRU(Least Recently Used)缓存。
LinkedHashMap<Integer, String> lruCache = new LinkedHashMap<>(16, 0.75f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, String> eldest) {
        return size() > 3; // 缓存容量为 3
    }
};
lruCache.put(1, "apple");
lruCache.put(2, "banana");
lruCache.put(3, "cherry");
lruCache.get(1); // 访问键为 1 的元素
lruCache.put(4, "date"); // 超出容量,删除最久未使用的元素 (2: banana)
  • 快速查找和有序遍历:LinkedHashMap 基于哈希表实现,查找性能极高(O(1)),同时保留了插入顺序或访问顺序。
2、不适合的场景
  • 频繁插入和删除:如果频繁插入和删除元素,可能会导致哈希冲突和扩容,影响性能。
  • 内存占用敏感:LinkedHashMap 需要额外的内存存储链表节点,内存占用较高。

5.4、TreeMap详解

5.4.1、概述

TreeMapJava中一个基于红黑树(Red-Black Tree)实现的集合类,一种自平衡二叉搜索树,它是 SortedMap接口的实现类。TreeMap不允许存储重复键,且键不允许为null(除非使用自定义比较器),并且键按照自然顺序或自定义顺序排序。TreeMap线程不安全,如果需要线程安全,可以使用Collections.synchronizedSortedMap

  • 底层数据结构:TreeMap 的底层是一个红黑树(Red-Black Tree),每个节点(Entry)包含键、值、左子节点、右子节点和父节点。红黑树是一种自平衡二叉搜索树,保证了插入、删除和查找操作的时间复杂度为 O(log n)。
  • 排序机制:元素按照自然顺序(Comparable 接口)或自定义顺序(Comparator 接口)排序。
TreeMap<Integer, String> map = new TreeMap<>(); // 自然顺序
TreeMap<String, Integer> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); // 自定义顺序
  • 动态扩容:TreeMap基于红黑树实现,不需要预先分配内存空间,因此没有固定的初始容量。每次添加元素时,动态调整红黑树的结构以保持平衡。
  • 哈希冲突解决方式:TreeMap不涉及哈希冲突,因为它基于红黑树实现,而不是哈希表。
    • 键的比较规则:如果没有指定比较器,则使用键的自然顺序(要求键实现 Comparable接口)。如果指定了比较器,则使用比较器定义的顺序。
    • 键的唯一性:TreeMap不允许重复键,如果尝试插入相同键的键值对,新值会覆盖旧值。
  • 时间复杂度
    • 添加元素:O(log n)。
    • 删除元素:O(log n)。
    • 查找元素:O(log n)。
    • 遍历元素:O(n)。

5.4.2、常用方法

  • put(K key, V value):添加键值对。
  • remove(Object key):删除指定键的键值对。
  • get(Object key):获取指定键的值。
  • subMap(K fromKey, K toKey):返回从 fromKey(包含)到 toKey(不包含)的子集。
  • size():返回键值对数量。
  • isEmpty():判断是否为空。
  • containsKey(Object key):判断是否包含指定键。
  • containsValue(Object value):判断是否包含指定值。
  • clear():清空所有键值对。

5.4.3、使用场景

1、适用场景
  • 需要有序集合:TreeMap保证元素的有序性,适合需要排序的场景。
  • 范围查询:TreeMap提供了高效的范围查询方法(如subMapheadMap、tailMap)。
  • 去重且排序:TreeMap不允许存储重复键,并且键有序,适合需要去重和排序的场景。
  • 统计分析:按某种顺序统计数据,如成绩排名、词频统计等。
  • 优先级队列:通过自定义比较器实现优先级排序。
  • 日志记录:按时间戳排序的日志存储。
2、不适合的场景
  • 频繁插入和删除:红黑树的平衡调整需要额外开销,频繁插入和删除操作性能较低。
  • 内存占用敏感:红黑树需要额外的内存存储节点和指针,内存占用较高。
  • 不需要排序:如果不需要排序,使用HashMapLinkedHashMap性能更高。

5.5、ConcurrentHashMap详解

5.5.1、概述

ConcurrentHashMapJava中一个线程安全的Map实现类,它是java.util.concurrent 包中的一部分。ConcurrentHashMap在高并发环境下提供了高效的读写操作,适合多线程场景。 ConcurrentHashMap基于分段锁(Segment Locking)或 CASCompare-And-Swap)机制实现。它允许存储键值对,键和值都不允许为null。在 JDK 1.7 中,使用分段锁实现;在 JDK 1.8 中,使用CASsynchronized实现。

  • 底层数据结构
    • JDK 1.7:使用分段锁(Segment Locking)机制,将整个哈希表分成多个段(Segment),每个段独立加锁。每个段是一个小的HashMap,包含一个数组和链表。
    • JDK 1.8:使用数组 + 链表 + 红黑树的结构。使用CASsynchronized实现线程安全。
      • 数组是核心,用于存储桶(Bucket)。
      • 每个桶中存放的是链表或红黑树,用于解决哈希冲突。
  • 初始容量和负载因子:默认初始容量为 16。默认负载因子为 0.75。
  • 动态扩容:当元素数量超过当前容量与负载因子的乘积时,ConcurrentHashMap 会自动扩容。
    • 扩容机制:
      • 新容量 = 旧容量 * 2。
      • 将旧哈希表中的元素重新哈希到新哈希表中。
    • 并发扩容:
      • 多个线程可以同时参与扩容操作。
      • 每个线程负责迁移一部分桶(Bucket),从而提高扩容效率。
  • 时间复杂度:
    • 添加元素:O(1)(平均情况下)。
    • 删除元素:O(1)(平均情况下)。
    • 查找元素:O(1)(平均情况下)。
    • 遍历元素:O(n)。

5.5.2、哈希冲突解决

详情请看5.2.5章节

  • 链地址法:在Java 8及之前的版本中,ConcurrentHashMap使用链地址法解决哈希冲突。
    • 当多个键映射到同一个桶时,这些键值对会被存储在一个链表中。
    • 每个桶中的链表节点由Node类表示,每个节点包含键、值、哈希值以及指向下一个节点的引用。
  • 红黑树优化:在 Java 8 中,ConcurrentHashMap引入了红黑树优化机制。
    • 当链表长度超过阈值(默认为 8),并且数组容量大于等于 64 时,ConcurrentHashMap会调用treeifyBin()方法将链表会转换为红黑树。
    • 数组容量小于 64,则会选择扩容而不是转换为红黑树,以减少冲突的可能性。
    • 如果红黑树中的节点数减少到 6 以下,红黑树会重新转换为链表
    • 红黑树是一种自平衡二叉搜索树,能够提供更高效的查找、插入和删除操作(时间复杂度为 O(log n))。

5.5.3、 线程安全的实现原理

1、原理
  • JDK 1.7:
    • 使用分段锁(Segment Locking)机制,将整个哈希表分成多个段(Segment),每个段独立加锁。
    • 每个段是一个小的HashMap,包含一个数组和链表。
    • 读操作不需要加锁,写操作只需要锁住对应的段。
  • JDK 1.8:
    • 使用CASsynchronized实现线程安全。
    • 每个桶(bucket)使用synchronized锁住链表或红黑树的头节点。
    • 读操作不需要加锁,写操作只需要锁住对应的桶。
2、补充概念
  • CASCompare-And-Swap):CAS是一种无锁的并发控制机制,通过原子操作实现线程安全。

    • 核心思想:比较并交换
      • 比较:检查目标内存位置的值是否与预期值相等。
      • 交换:如果相等,则将目标内存位置的值更新为新值;如果不相等,则不进行任何操作。
    • 原理:CAS 操作依赖于底层硬件的支持,通常由 CPU 提供原子指令实现(如 x86 架构中的 CMPXCHG 指令)。这些指令保证了操作的原子性,即在多线程环境下不会被中断。
    • 优点:
      • 高效性:CAS 不需要加锁,减少了线程间的阻塞和上下文切换开销。
      • 非阻塞性:线程不会因等待锁而被挂起,从而提高了系统的吞吐量。
    • 缺点:
      • ABA 问题:假设某个变量的值从 A 变为 B,然后又变回 A。在这种情况下,CAS 操作可能会误认为该值没有发生变化,从而导致错误的结果。解决方案包括引入版本号或时间戳(如 AtomicStampedReference)。
      • 循环时间过长:如果 CAS 操作失败,线程通常会进入自旋状态(不断重试),这可能导致 CPU 占用率过高。可以通过退避算法降低重试频率。
  • 细粒度锁:细粒度锁是一种锁的设计策略,通过缩小锁的作用范围,减少锁的竞争,从而提高并发性能。

    • 优势对比:
      • 在传统的粗粒度锁中,整个数据结构会被锁定,所有线程必须等待锁释放后才能访问。
      • 在细粒度锁中,锁的作用范围被限制到更小的粒度(如单个桶、节点或对象),允许多个线程同时操作不同的部分。
    • 优点:
      • 高并发性能:允许多个线程同时操作不同的桶,减少了锁的竞争。
      • 灵活性:可以根据实际需求调整锁的粒度,平衡性能和复杂性。
    • 缺点:
      • 复杂性增加:细粒度锁的设计和实现比粗粒度锁更复杂,可能引入更多的边界条件和潜在问题。
      • 死锁风险:如果多个线程需要获取多个锁,并且获取顺序不同,可能会导致死锁。
  • 转发节点Forwarding Node):ConcurrentHashMapJava 8及之后版本中引入的一种特殊节点,用于在并发扩容过程中协调多个线程的操作。它的主要作用是确保在扩容期间,读写操作能够正确处理正在迁移的桶(Bucket),从而避免死锁或数据不一致问题。

    • 作用:在ConcurrentHashMap的扩容过程中
      • 原数组中的元素需要重新计算哈希值,并迁移到新数组中。
      • 如果某个桶正在被迁移,其他线程可能仍然尝试访问该桶。
      • 为了保证线程安全,ConcurrentHashMap使用Forwarding Node标记该桶的状态。
    • 功能:
      • 标记迁移状态:当一个桶正在被迁移时,Forwarding Node会替换原来的链表或红黑树,通知其他线程该桶正在迁移。
      • 重定向操作:其他线程在访问该桶时,会通过Forwarding Node被重定向到新数组中对应的位置。
      • 避免死锁:通过Forwarding Node,确保多个线程可以安全地参与扩容操作,而不会因等待锁而导致死锁。
    • 工作流程
      1. 扩容过程中的使用:当某个线程开始扩容时,它会先创建一个新的数组。然后,它会逐个迁移原数组中的桶到新数组中。在迁移某个桶时,该桶会被替换为一个 Forwarding Node,指向新数组。
      2. 线程的处理:当其他线程尝试访问一个已经被标记为Forwarding Node的桶时,它会检测到该桶是一个Forwarding Node,然后通过Forwarding NodenextTable字段,直接跳转到新数组中查找目标键值对,如果目标键值对不存在,则会在新数组中插入新节点。
      3. 并发扩容的支持:多个线程可以同时参与扩容操作,每个线程负责迁移一部分桶。通过Forwarding Node,确保所有线程都能正确处理正在迁移的桶,避免数据丢失或重复。
    • 优点
      • 高效性:通过Forwarding Node,多个线程可以安全地参与扩容操作,减少了锁的竞争。
      • 线程安全性:确保在扩容期间,读写操作能够正确处理正在迁移的桶,避免数据不一致问题。
      • 避免死锁:通过重定向机制,防止多个线程因等待锁而导致死锁。
3、线程安全实现细节
  • 插入操作
    • 步骤:
      • 计算键的哈希值,并定位到对应的桶。
      • 如果桶为空,则通过 CAS 尝试初始化该桶。
      • 如果桶中有元素,则遍历链表或红黑树,检查是否已存在相同的键。
      • 如果存在相同的键,则更新其值。
      • 如果不存在相同的键,则尝试通过 CAS 添加新节点。
      • 如果 CAS 失败,则使用细粒度锁(仅锁定当前桶)完成插入操作。
    • 线程安全性:使用 CAS 实现无锁操作,减少锁的竞争。如果 CAS 失败,则使用细粒度锁,确保操作的原子性。
  • 删除操作
    • 步骤:
      • 计算键的哈希值,并定位到对应的桶。
      • 遍历链表或红黑树,找到需要删除的节点。
      • 如果找到目标节点,则通过 CAS 或细粒度锁完成删除操作。
    • 线程安全性:删除操作与插入操作类似,优先使用 CAS,失败时使用细粒度锁。
  • 扩容操作
    • 步骤:
      • 当元素数量超过阈值(threshold = capacity * loadFactor)时触发扩容。
      • 创建一个新的数组,大小为原数组的两倍。
      • 将原数组中的所有元素重新计算哈希值,并迁移到新数组中。
      • 在迁移过程中,使用转发节点(Forwarding Node)协调多个线程的操作。
    • 并发扩容:每个线程负责迁移一部分桶,从而提高扩容效率。转发节点确保其他线程能够正确处理正在迁移的桶。
  • 遍历操作
    • Java 8中,ConcurrentHashMap提供了多种遍历方式(如forEach()keySet() 等),这些方法在遍历时不会锁定整个哈希表。
    • 遍历操作可能会看到部分未完成的迁移结果,但不会导致数据不一致。

5.5.4、 常用方法

  • put(K key, V value):添加键值对。
  • remove(Object key):删除指定键的键值对。
  • get(Object key):获取指定键的值。
  • size():返回键值对数量。
  • isEmpty():判断是否为空。
  • containsKey(Object key):判断是否包含指定键。
  • containsValue(Object value):判断是否包含指定值。
  • clear():清空所有键值对。

5.5.5、使用场景

1、适合的场景
  • 高并发环境:ConcurrentHashMap 是线程安全的,适合高并发环境下的键值对存储和操作。
  • 缓存系统:适合用作缓存系统,支持高效的并发读写操作。
  • 数据量较大:ConcurrentHashMap 的扩容机制和哈希表特性使其适合存储大量数据。
2、不适合的场景
  • 需要有序集合:ConcurrentHashMap 不保证元素的顺序,如果需要有序集合,可以使用 ConcurrentSkipListMap。
  • 内存占用敏感:ConcurrentHashMap 需要额外的内存存储链表/红黑树节点,内存占用较高。

5.6、Hashtable详解

5.6.1、概述

Hashtable是 Java 中一个古老的集合类,它是Map接口的实现类,基于哈希表(Hash Table)实现。Hashtable是线程安全的,但性能较低,因此在现代Java开发中更推荐使用ConcurrentHashMap。Hashtable允许存储键值对,键和值都不允许为null,它不保证元素的顺序(从Java 8开始,迭代顺序可能与插入顺序一致,但不保证)。由于性能问题,现代Java开发中更推荐使用ConcurrentHashMap

  • 底层数据结构:Hashtable的底层是一个Entry<?,?>[]数组,每个Entry是一个键值对。当发生哈希冲突时,Hashtable使用链表来存储冲突的键值对。
  • 初始容量和负载因子:默认初始容量为 11,默认负载因子为 0.75。
  • 动态扩容:当元素数量超过当前容量与负载因子的乘积时,Hashtable会自动扩容。
  • 扩容机制:
    • 新容量 = 旧容量 * 2 + 1。
    • 将旧哈希表中的元素重新哈希到新哈希表中。
  • 哈希冲突解决方式:Hashtable使用链地址法(Separate Chaining)解决哈希冲突(详情请看5.2.5章节)。
  • 时间复杂度
    • 添加元素:O(1)(平均情况下)。
    • 删除元素:O(1)(平均情况下)。
    • 查找元素:O(1)(平均情况下)。
    • 遍历元素:O(n)。

5.6.2、常用方法

  • put(K key, V value):添加键值对。
  • remove(Object key):删除指定键的键值对。
  • get(Object key):获取指定键的值。
  • size():返回键值对数量。
  • isEmpty():判断是否为空。
  • containsKey(Object key):判断是否包含指定键。
  • containsValue(Object value):判断是否包含指定值。
  • clear():清空所有键值对。

5.6.3、使用场景

1、适用场景
  • 多线程环境:Hashtable 是线程安全的,适合在多线程环境下使用。如果不需要线程安全,建议使用 HashMap。
  • 简单的同步需求:如果只需要简单的线程安全功能,而不需要复杂的并发控制(如分段锁或读写锁),Hashtable 是一个简单易用的选择。
2、不适合的场景
  • 高并发场景:在高并发场景下,Hashtable的性能较差,因为所有操作都需要竞争同一把锁。此时更适合使用 ConcurrentHashMap
  • 需要高性能读操作:如果读操作远多于写操作,Hashtable 的性能不如ConcurrentHashMap
  • 需要复杂的并发控制:如果需要更细粒度的并发控制(如读写锁或分段锁),Hashtable无法满足需求。

5.7、WeakHashMap详解

5.7.1、概述

WeakHashMapJava中一个特殊的Map实现类,它是基于弱引用(Weak Reference)实现的哈希表。WeakHashMap的键是弱引用,当键不再被强引用时,垃圾回收器会自动回收这些键值对。WeakHashMap 线程不安全,如果需要线程安全,可以使用Collections.synchronizedMap

  • 底层数据结构:WeakHashMap的底层是一个Entry<?,?>[]数组,每个Entry是一个键值对。键是弱引用(WeakReference),值是强引用。当键不再被强引用时,垃圾回收器会自动回收这些键值对。
  • 初始容量和负载因子:默认初始容量为16,默认负载因子为0.75
  • 动态扩容:当元素数量超过当前容量与负载因子的乘积时,WeakHashMap会自动扩容。
  • 扩容机制:
    • 新容量 = 旧容量 * 2。
    • 将旧哈希表中的元素重新哈希到新哈希表中。
  • 哈希冲突解决方式:WeakHashMap使用链地址法(Separate Chaining)解决哈希冲突。当发生哈希冲突时,冲突的键值对会存储在同一个桶(bucket)中,形成一个链表(详情请看5.2.5章节)。
  • 时间复杂度
    • 添加元素:O(1)(平均情况下)。
    • 删除元素:O(1)(平均情况下)。
    • 查找元素:O(1)(平均情况下)。
    • 遍历元素:O(n)。

5.7.2、常用方法

  • put(K key, V value):添加键值对。
  • remove(Object key):删除指定键的键值对。
  • get(Object key):获取指定键的值。
  • size():返回键值对数量。
  • isEmpty():判断是否为空。
  • containsKey(Object key):判断是否包含指定键。
  • containsValue(Object value):判断是否包含指定值。
  • clear():清空所有键值对。

5.7.3、使用场景

1、适用场景
  • 缓存系统:WeakHashMap 适合用作缓存系统,当键不再被强引用时,垃圾回收器会自动回收这些键值对。
  • 防止内存泄漏:WeakHashMap 可以防止因键未被释放而导致的内存泄漏。
  • 监听器管理:避免因监听器未注销而导致的内存泄漏问题。
  • 资源管理:管理临时资源,确保资源不会因长期持有而占用过多内存。
2、不适用场景
  • 需要强引用键:如果键需要长期存在,不适合使用WeakHashMap
  • 频繁插入和删除:如果频繁插入和删除元素,可能会导致哈希冲突和扩容,影响性能。

5.8、EnumMap详解

5.8.1、概述

EnumMap是 Java 中一个专门用于枚举类型的高效集合类,它是Map接口的实现类。EnumMap基于枚举常量的顺序实现,性能极高,且内存占用非常低。EnumMap允许存储键值对,键必须是同一枚举类型的常量,值可以是任意类型。它线程不安全,如果需要线程安全,可以使用Collections.synchronizedMap

  • 底层数据结构:EnumMap的底层是一个Object[]数组,数组的索引对应枚举常量的顺序。
  • 初始容量:EnumMap的容量固定为枚举类型的常量数量。不需要动态扩容,因为枚举类型的常量数量是固定的。
  • 动态扩容:EnumMap不需要动态扩容,因为枚举类型的常量数量是固定的。
  • 哈希冲突解决方式:EnumMap不涉及哈希冲突,因为它基于枚举常量的顺序实现,而不是哈希表。
  • 时间复杂度:
    • 添加元素:O(1)
    • 删除元素:O(1)
    • 查找元素:O(1)
    • 遍历元素:O(n)

5.8.2、常用方法

  • put(K key, V value):添加键值对。
  • remove(Object key):删除指定键的键值对。
  • get(Object key):获取指定键的值。
  • size():返回键值对数量。
  • isEmpty():判断是否为空。
  • containsKey(Object key):判断是否包含指定键。
  • containsValue(Object value):判断是否包含指定值。
  • clear():清空所有键值对。

5.8.3、使用场景

1、适用场景
  • 枚举类型集合:EnumMap 专门用于枚举类型,适合存储同一枚举类型的键值对。
  • 高效集合操作:EnumMap 基于枚举常量的顺序实现,性能极高,适合需要高效集合操作的场景。
  • 内存占用敏感:EnumMap 的内存占用非常低,适合内存占用敏感的场景。
2、不适用场景
  • 非枚举类型集合:EnumMap 只能存储枚举类型的键,不适合存储非枚举类型的键。
  • 需要存储 null 键:EnumMap 不允许存储 null 键。

5.9、IdentityHashMap详解

5.9.1、概述

IdentityHashMapJava中一个特殊的Map实现类,它是基于对象引用相等性(==)而不是对象值相等性(equals)实现的哈希表。IdentityHashMap使用System.identityHashCode来计算哈希值,适合需要区分对象引用的场景。IdentityHashMap允许存储键值对,键和值都可以为null。它线程不安全,如果需要线程安全,可以使用 Collections.synchronizedMap

  • 底层数据结构:IdentityHashMap的底层是一个Object[]数组,数组的每个元素交替存储键和值。
  • 初始容量和负载因子:默认初始容量为 32。默认负载因子为 2/3。
  • 动态扩容:当元素数量超过当前容量与负载因子的乘积时,IdentityHashMap 会自动扩容。
  • 扩容机制:
    • 新容量 = 旧容量 * 2。
    • 将旧哈希表中的元素重新哈希到新哈希表中。
  • 时间复杂度
    • 添加元素:O(1)(平均情况下)。
    • 删除元素:O(1)(平均情况下)。
    • 查找元素:O(1)(平均情况下)。
    • 遍历元素:O(n)

5.9.2、开放地址法(Open Addressing)解决哈希冲突

当发生哈希冲突时,通过某种探测方法(如线性探测、二次探测、双重哈希等)在哈希表中寻找下一个空闲的位置。所有的键值对都存储在哈希表的数组中,没有额外的链表或树结构。

开放地址法的实现方式

  • 线性探测(Linear Probing):当发生冲突时,依次检查下一个位置,直到找到一个空闲的位置。
    • 探测公式:h(k,i) = (h(k)+i) mod m
      • h(k)是哈希函数。
      • i是探测次数(从 0 开始)。
      • m是哈希表的大小。
    • 优点
      • 实现简单。
      • 缓存友好,因为探测的位置是连续的。
    • 缺点
      • 容易产生 聚集(Clustering)现象,即连续的冲突导致数据集中在某个区域,降低性能。
    • 示例
      • 假设哈希表大小为10,哈希函数为h(k) = k mod 10
        1. 插入键值对(5, A):位置5空闲,直接插入。
        2. 插入键值对(15, B):位置5已被占用,检查位置6,空闲,插入。
        3. 插入键值对(25, C):位置56已被占用,检查位置7,空闲,插入。
  • 二次探测(Quadratic Probing):当发生冲突时,通过二次函数计算下一个探测位置。
    • 探测公式:h(k,i) = (h(k) + c1*i + c2*i*i) mod m
      • c1c2是常数。
      • i是探测次数(从 0 开始)。
      • m是哈希表的大小。
    • 优点
      • 减少线性探测的聚集现象。
    • 缺点
      • 仍然可能产生 二次聚集(Secondary Clustering)。
      • 需要选择合适的c1c2以避免探测位置重复。
    • 示例:假设哈希表大小为10,哈希函数为h(k) = k mod 10c1 = 1c2 = 1
      1. 插入键值对(5, A):位置5空闲,直接插入。
      2. 插入键值对(15, B):位置5已被占用,检查位置(5+1+1) mod 10 = 7,空闲,插入。
      3. 插入键值对(25, C):位置5已被占用,检查位置(5+1+1) mod 10 = 7,已被占用,检查位置(5+2+4) mod 10 = 1,空闲,插入
  • 双重哈希(Double Hashing):当发生冲突时,使用第二个哈希函数计算探测步长。
    • 探测公式:h(k,i)=(h1(k) + i*h2(k)) mod m
      • h1(k)是第一个哈希函数。
      • h2(k)是第二个哈希函数。
      • i是探测次数(从0开始)。
      • m是哈希表的大小。
    • 优点:
      • 减少聚集现象,探测序列更加分散。
      • 性能优于线性探测和二次探测。
    • 缺点:
      • 需要设计两个哈希函数。
      • 实现复杂度较高。
    • 举例:假设哈希表大小为 10,哈希函数为 h1(k) = k mod 10h2(k) = 7 - (k mod 7)
      1. 插入键值对(5, A):位置5空闲,直接插入。
      2. 插入键值对(15, B):位置5已被占用,步长为 h2(15) = 7 - ( 15 mod 7 ) = 7 - 1 = 6 ,检查位置(5 + 6) mod 10 = 1,空闲,插入。
      3. 插入键值对(25, C):位置5已被占用,步长为h2(25) = 7 − (25 mod 7 ) = 7 − 4 = 3 ,检查位置 (5+3) mod 10 = 8,空闲,插入。

5.9.3、常用方法

  • put(K key, V value):添加键值对。
  • remove(Object key):删除指定键的键值对。
  • get(Object key):获取指定键的值。
  • size():返回键值对数量。
  • isEmpty():判断是否为空。
  • containsKey(Object key):判断是否包含指定键。
  • containsValue(Object value):判断是否包含指定值。
  • clear():清空所有键值对。

5.9.5、使用场景

1、适用场景
  • 需要区分对象引用:IdentityHashMap使用对象引用相等性(==)而不是对象值相等性(equals),适合需要区分对象引用的场景。
  • 对象生命周期管理:适合用于管理对象的生命周期,例如缓存系统中需要区分相同值的不同对象。
  • 避免递归结构:当键值对中可能存在循环引用或递归结构时,IdentityHashMap可以避免因equals()方法引发的无限递归问题。
  • 图算法:在实现图算法时,可以使用IdentityHashMap来表示节点之间的关系,避免因 equals()方法导致的误判。
2、 不适用场景
  • 需要对象值相等性:如果需要使用对象值相等性(equals),不适合使用IdentityHashMap
  • 内存占用敏感:IdentityHashMap需要额外的内存存储键值对,内存占用较高。

5.10、Collections.synchronizedMap详解

5.10.1、概述

Collections.synchronizedMapJava提供的一个工具方法,用于将普通的 Map 转换为线程安全的同步集合。它通过包装现有的Map实现类(如HashMapTreeMap),并为其所有方法添加同步锁,从而保证线程安全。

  • 线程安全:Collections.synchronizedMap 通过为所有方法添加同步锁(synchronized)来保证线程安全。
  • 底层实现:
    • Collections.synchronizedMap并不直接实现Map接口,而是通过包装现有的Map实现类(如HashMapTreeMap)来提供线程安全的功能。
    • 它内部维护了一个mutex对象(锁对象),所有方法都通过synchronized(mutex) 实现同步。
  • 性能:由于所有方法都使用了同步锁,Collections.synchronizedMap的性能较低。在高并发场景下,可能会成为性能瓶颈。

5.10.2、使用场景

1、适用场景
  • 多线程环境:当多个线程需要共享和修改同一个Map时,可以使用 Collections.synchronizedMap来保证线程安全。
  • 简单的同步需求:如果只需要简单的线程安全功能,而不需要复杂的并发控制(如分段锁或读写锁),Collections.synchronizedMap是一个简单易用的选择。
  • 兼容现有代码:如果现有代码已经使用了HashMapTreeMap,可以通过 Collections.synchronizedMap快速实现线程安全,而无需修改大量代码。
2、不适用场景
  • 高并发场景:在高并发场景下,Collections.synchronizedMap的性能较差,因为所有操作都需要竞争同一把锁。此时更适合使用ConcurrentHashMap
  • 需要高性能读操作:如果读操作远多于写操作,Collections.synchronizedMap的性能不如ConcurrentHashMap
  • 需要复杂的并发控制:如果需要更细粒度的并发控制(如读写锁或分段锁),Collections.synchronizedMap无法满足需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值