容器总览

ℹ️定位说明

本篇是 02-Java容器 的入口与速览:一图一表建立"两大体系(Collection 单值 / Map 键值对)"的整体认知,四大节分别是 List / Set / Queue / Map 的浓缩版(详篇见各系列子目录)。学习动线:本篇 → List 系列Set 系列Queue 系列Map 系列选型总对比高频面试题(收官)。本篇同时保留了 C++ STL 类比视角,适合有 C++ 背景时快速对照。

整体认知

Java 容器(Collections)是用来“装对象的对象”,即集合类框架,整体由 两大核心接口 构成:

一、Collection 接口 —— 单值集合体系

用于表示一组元素的“集合”,即一个一个元素地存储。

✅ 类比 C++ STL 中的 vectorlistsetqueue

主要子接口与实现类:

image-daf2dd0c
接口/实现类 特点说明 类比 C++ STL
List 有序、可重复,支持索引 vector, list
├── ArrayList 动态数组实现,查找快、增删慢 vector
├── LinkedList 双向链表实现,查找慢、增删快 list, deque
├── Vector 线程安全,老旧,功能和 ArrayList 类似
Set 无序/有序、不可重复 set, unordered_set
├── HashSet 基于哈希表,无序 unordered_set
├── LinkedHashSet 插入顺序有序 无完全对应
├── TreeSet 基于红黑树,有序 set
Queue / Deque 队列/双端队列 queue, deque
├── PriorityQueue 优先级队列,最小堆 priority_queue
├── ArrayDeque 高效的双端队列 deque

二、Map:键值对映射体系

存储 key-value 形式的数据,常用于快速查找,不属于 Collection 接口体系。

✅ 类比 C++ STL 中的 mapunordered_map

image-c00a750a

常见实现类:

类名 特点说明 类比 C++ STL
HashMap<K, V> 无序、基于哈希表,查找快,允许 null unordered_map
LinkedHashMap<K, V> 基于哈希表 + 链表,有插入顺序 无严格对应
TreeMap<K, V> 有序、基于红黑树,按 key 自然顺序或自定义排序 map
Hashtable<K, V> 线程安全的早期实现,已不推荐使用

学习顺序:

  • List 接口(线性结构)
    • ArrayList:基于动态数组,支持随机访问,增删效率相对较低。
    • LinkedList:基于双向链表,插入删除快,但不支持快速随机访问。
    • Vector:线程安全的动态数组,已较少使用,了解其同步机制即可。
  • Set 接口(去重集合)
    • HashSet:基于 HashMap,无序、唯一。
    • LinkedHashSet:保留插入顺序。
    • TreeSet:基于红黑树,自动排序。
  • Queue / Deque 接口(队列结构)
    • Queue(如 PriorityQueue):优先级队列,非 FIFO。
    • Deque(如 ArrayDequeLinkedList):双端队列,支持栈、队列双重操作。
  • Map 接口(键值对映射)
    • HashMap:最常用,基于哈希表。
    • LinkedHashMap:有序版本的 HashMap。
    • TreeMap:自动排序的 Map,基于红黑树。

List

List 是 Java 集合框架中最常用的接口之一,继承自Collection,具有以下核心特性:

  • 有序性:元素按插入顺序存储,支持通过索引(下标)访问元素。
  • 可重复性:允许存储重复元素。
  • 丰富的操作接口:提供get(int index)add(int index, E element)remove(int index)等基于索引的操作方法,方便数据的随机访问和位置调整。

核心实现类:ArrayListLinkedListVector,分别基于动态数组、双向链表、线程安全动态数组实现,适用场景各异。

ArrayList

  Java中 `ArrayList` 是基于**动态数组**实现的可变长度顺序表,支持快速随机访问(O(1)),但插入、删除需移动元素,效率较低(O

(n)),是查找频繁、插入删除较少场景的首选容器。

  它底层使用 Object[] 存储元素,采用懒惰初始化机制:无参构造时初始化为空数组,首次添加元素时才分配默认容量

10。添加元素时会先检查剩余空间是否充足,若不足则触发扩容,扩容策略为oldCapacity + (oldCapacity >> 1),约等于原容量的 1.5 倍,若仍不足则直接取所需容量。由于扩容需要通过 Arrays.copyOf() 复制原数组,代价较高,建议通过预估容量,减少扩容频率。

  另外,`ArrayList` 是非线程安全的,多线程场景可通过`Collections.synchronizedList(new ArrayList<>())`或并发容器`CopyOnWriteArrayList`(写时复制,适用于读多写少场景)保证安全。

  它支持 fail-fast 机制:迭代过程中若集合结构被修改(如增删元素),会通过`modCount`变量校验触发`ConcurrentModificationException`,防止脏读。

在序列化方面,ArrayList 将底层数组声明为transient,避免默认序列化整个数组,同时通过自定义writeObjectreadObject方法,仅序列化实际存储的有效元素(即size范围内的元素),优化了空间使用效率。

常见问法:

  • ArrayList 的默认初始容量是多少?何时分配?

    • 默认初始容量为 10,但采用懒惰初始化—— 无参构造时底层数组初始化为空数组(DEFAULTCAPACITY_EMPTY_ELEMENTDATA),首次添加元素时才分配 10 的容量
  • 为什么扩容是 1.5 倍而非 2 倍?

    • 1.5 倍是空间与效率的折中:避免像 Vector 2 倍扩容那样浪费过多空间,同时减少频繁扩容的性能开销(例如原容量 10 → 15 比 10 → 20 更节省内存)。
  • ArrayList 的 trimToSize() 方法有什么用?

    • 将底层数组容量缩为当前元素数量(size),释放未使用空间。适用于固定大小列表以节省内存,但可能触发数组复制(O (n))。
  • ArrayList 插入元素时,数组越界是如何处理的?

    • 添加元素前会调用 ensureCapacityInternal() 检查容量,若不足则触发扩容(通过 grow() 方法),确保数组不会越界。
  • ArrayList 的 modCount 有什么作用?

    • 记录集合结构修改次数(增删操作),用于实现 fail-fast 机制。迭代器初始化时保存 modCount,遍历时检测到不一致则抛出 ConcurrentModificationException
  • ArrayList的set(int index, E element)会触发Fail-Fast吗?

    • 不会,**set()**仅替换元素,不改变结构(modCount不变),不会触发异常。
  • 如何安全删除 ArrayList 中的元素?

    • 使用 迭代器的 remove() 方法,它会同步更新 modCount,避免触发异常。例如:
    Iterator<String> it = list.iterator();
    while (it.hasNext()) {
        if (条件) {
            it.remove(); // 安全删除
        }
    }
    ```
- **ArrayList 与** `Arrays.asList()` **返回的列表有什么区别?**

  - `Arrays.asList()` 返回的是 **固定大小列表**,不支持增删操作(调用会抛 `UnsupportedOperationException`);
  - ArrayList 是动态可变的,支持扩容和增删。
- **ArrayList 可以存储基本数据类型吗?**

  - 不能直接存储,需使用包装类(如 `Integer`、`Long`)。例如:

```java
    ArrayList<Integer> list = new ArrayList<>(); // 正确
    // ArrayList<int> list = new ArrayList<>(); // 错误
    ```

    原因见泛型。
- **ArrayList 在多线程下的安全替代方案有哪些?**

  - **同步包装**:`Collections.synchronizedList(new ArrayList<>())`;
  - **并发容器**:`CopyOnWriteArrayList`(写时复制,适用于读多写少)。
- **ArrayList 的** `addAll()` **方法效率如何?**

  - 批量添加元素时,会先计算所需容量并扩容一次(若需要),避免多次扩容。但复制数组仍需 O (n) 时间,整体效率高于循环单元素添加。

### LinkedList

       Java中`LinkedList`是基于**双向链表**实现的集合类,底层由节点(Node)构成,每个节点存储元素值、前驱和后继指针。它支持两端高效插入/删除(O(1)),但随机访问需遍历链表(O(n)),即使通过二分法优化遍历方向(前半部分从头找,后半部分从尾找),本质仍为线性查找,此外,它兼具`List`和`Deque`接口功能,可作为列表、队列或栈使用,但真要用到队列或栈还是推荐使用`ArrayDeque`。
适用于头尾操作频繁场景(如消息队列、栈),或数据动态变化、无需随机访问的场景。

        添加元素仅需调整指针,无需扩容;删除时主动断开节点引用(如`x.item = null`),避免内存泄漏。支持`null`元素,非线程安全,遍历中结构修改会触发fail-fast机制(抛ConcurrentModificationException),需用迭代器`remove()`安全操作。

        多线程下可通过`Collections.synchronizedList()`包装,或改用并发容器(如无锁的`ConcurrentLinkedQueue`、阻塞式的`LinkedBlockingQueue`)。

**常见问法:**

- LinkedList 和 ArrayList 有什么区别?
  - 前者是双向链表(增删快、查找慢),后者是动态数组(查找快、增删慢)。
- LinkedList 是怎么实现插入删除的?为什么效率高?
  - 头尾操作仅改指针(O (1)),无需移动元素。
- 为什么不推荐用 LinkedList 查找元素?
  - 无索引,需遍历链表(O (n)),效率低。
- 如何安全删除 LinkedList 中的元素?
  - 用迭代器 it.remove(),避免直接修改集合。
- LinkedList 是线程安全的吗?并发场景怎么使用?
  - 非线程安全,并发时用`Collections.synchronizedList()`或并发队列(如`ConcurrentLinkedQueue`/`LinkedBlockingQueue`)。

### Vector

      Vector 是 Java 早期基于动态数组实现的线程安全列表,底层通过 `Object[]` 存储元素。其默认初始容量为
10,扩容时按原容量的 2 倍增长(也可通过参数指定增量),支持快速随机访问(O(1)),但插入、删除操作需移动元素,效率较低(O(n))。

     所有公共方法通过 `synchronized` 修饰保证线程安全,适用于多线程环境下需要线程安全且插入删除不频繁的场景(如共享数据读写)。但由于同步机制带来的性能开销,其单线程性能低于
ArrayList,现代开发中更推荐使用 `ArrayList` 结合 `Collections.synchronizedList()` 或并发容器(如 `CopyOnWriteArrayList`)替代。

**常见问法:**

- Vector 的底层数据结构是什么?
  - 基于动态数组(Object [])实现,类似 ArrayList,但线程安全。
- Vector 如何实现线程安全?
  - 方法通过 `synchronized` 关键字修饰(如 `add()`、`get()` 等),确保多线程下操作的原子性。
- Vector 和 ArrayList 的主要区别是什么?
  - 线程安全:Vector 线程安全(同步),ArrayList 非线程安全(异步)。
  - 扩容策略:Vector 默认扩容为原容量的 2 倍(可通过构造方法指定扩容倍数),ArrayList 为 1.5 倍。
  - 性能:Vector 因同步机制性能略低,适用于多线程场景;ArrayList 适用于单线程或线程安全可控场景。
- Vector 的扩容机制是怎样的?默认扩容倍数是多少?
  - 当元素数量超过容量时触发扩容,默认新容量为 原容量的 2 倍(可通过 `capacityIncrement` 参数自定义倍数)。
- 为什么 Vector 的扩容默认是 2 倍,而 ArrayList 是 1.5 倍?
  - Vector 早期设计更注重减少扩容频率(牺牲空间换时间),ArrayList 更注重节省内存(1.5 倍更折中)。
- Vector 适合在什么场景下使用?
  - 多线程环境中需要线程安全的列表操作(如共享数据的读写)。
  - 需要频繁扩容但对内存不敏感的场景(因扩容倍数较大,空间浪费可能更多)。
- Vector 的缺点是什么?
  - 同步机制导致性能较低,单线程场景下不如 ArrayList 高效。
  - 扩容时默认增长 2 倍,可能比 ArrayList 更占内存。
  - 适配性差,现代多线程场景更推荐并发容器(如 CopyOnWriteArrayList)。
- Vector 是否支持 null 元素?
  - 支持,允许存储 null(与 ArrayList 一致)。
- Vector 如何遍历?是否支持 fail-fast 机制?
  - 支持 `for`、`foreach`、`Iterator` 等遍历方式,不支持
    fail-fast(迭代过程中结构修改不会抛出异常,但可能读取到不一致的数据)。
- 如何提升 Vector 的性能?
  - 预指定初始容量,减少扩容次数;
  - 若单线程场景,可转为 ArrayList(需手动处理线程安全)。
- **Vector的现代替代方案?**

  - 多线程场景 → **CopyOnWriteArrayList**(读多写少)、**ConcurrentLinkedQueue**(高并发队列)。
  - 需同步的List → **Collections.synchronizedList(new ArrayList<>())**。

### 对比

1. 线程安全:

   - Vector 是唯一线程安全的列表,但性能代价高;
   - ArrayList/LinkedList 可通过 `Collections.synchronizedList()` 或并发容器(如 `CopyOnWriteArrayList`)实现线程安全。
2. 数据结构特性:

   - ArrayList 适合 “随机访问优先” 的场景(如分页查询、数组操作);
   - LinkedList 适合 “增删优先” 的场景(如链表操作、双端队列);
   - Vector 适合 “多线程 + 随机访问” 的场景(如旧系统中的线程安全列表)。
3. 内存占用:
   - ArrayList/Vector 紧凑(数组无额外指针)
   - LinkedList 较高(每个节点含前驱 / 后继指针)
4. 扩容与性能:

   - ArrayList 扩容更节省内存(1.5 倍),Vector 扩容更激进(2 倍);
   - LinkedList 无扩容问题,但查找性能差。
5. 现代开发建议:

   - 单线程场景:ArrayList(首选)或 LinkedList(增删密集);
   - 多线程场景:ArrayList + 同步工具 或 并发容器(如 `CopyOnWriteArrayList`),避免直接使用
     Vector(性能较低)。

## Set

<a href="/vault/2-Learning/04-Java/02-Java%E5%AE%B9%E5%99%A8/02-Set%E7%B3%BB%E5%88%97/00-Set%E6%80%BB%E8%A7%88">Set</a> 是 Java 集合框架中的一个核心接口,继承自 Collection,主要特点是:

- **不允许重复元素**(根据 equals/hashCode 判定)
- **无索引访问**(不支持 get(index))
- **常用于去重或存储唯一性数据**

核心实现类:`HashSet`、`LinkedHashSet`、`TreeSet`。

### HashSet

`HashSet` **是 Java 中基于** `HashMap` **实现的无序集合,其底层通过将元素作为键(Key)存储,值(Value)统一使用静态常量** `PRESENT` **作为占位符。**

为了确保元素的唯一性,HashSet 依赖元素的 `hashCode()` 和 `equals()` 方法:插入元素时,先通过 `hashCode()` 计算哈希值定位桶(Bucket),若该位置为空则直接插入;若已有元素,则再通过 `equals()` 判断是否相等,若相等则视为重复,不插入,若不相等则通过链地址法(链表或红黑树)进行存储。

需要特别注意的是:如果只重写了 `equals()` 而未重写 `hashCode()`,相等的对象可能被散列到不同桶中,导致集合中出现重复元素;反之亦然。因此,自定义类作为
HashSet 元素时,必须同时重写这两个方法,且需保证相等的对象具有相同的哈希值。

在哈希分布良好、冲突较少的情况下,HashSet 的 `add`、`remove` 和 `contains` 操作的平均时间复杂度为 **O(1)**。在极端情况下(如所有元素哈希值相同),JDK
7 及之前版本中退化为链表,操作复杂度变为 O(n);而在 JDK 8 之后,当链表长度 ≥ 8 且桶总数 ≥ 64 时,会自动将该桶结构转换为红黑树,从而将查找复杂度降低为
O(log n)。

HashSet 的默认负载因子为 0.75,意味着当元素数量超过容量的 75% 时将触发扩容(新容量为原来的 2 倍)。合理地预估并设置初始容量,有助于减少扩容次数,降低
rehash 带来的性能损耗。

此外,HashSet:

- **允许存储一个 null 元素**
- **不保证元素的存储顺序**
- **不是线程安全的**

常用于高效去重、快速存在性判断、集合运算(如交集、并集、差集)等场景。

**常见问法:**

- HashSet 中是如何判断重复元素的?
  - 先用 hashCode 定位桶,再用 equals 判断。
- HashSet 能存 null 吗?
  - 可以,最多一个。
- 为什么 HashSet 查找快?
  - 基于哈希表,平均复杂度 O(1)。
- JDK8 之后 HashSet 做了哪些优化?
  - 链表转红黑树,提高高冲突场景的性能。
- JDK 8 链表转红黑树的阈值为何是 8?

  - 哈希冲突遵循泊松分布,链表长度达到 8 的概率极低(约 1e-6)。此阈值是对极端情况的保护,避免恶意哈希攻击导致性能骤降。
- 为何扩容时容量翻倍?

  - 位运算优化:翻倍后新容量为 2 的幂,计算桶位置时可通过 hash & (capacity - 1) 替代取模运算,提升效率。
  - 分散冲突:翻倍扩容使元素重新散列到新桶,可能减少链表长度或树化概率。
- HashSet 是线程安全的吗?如何变线程安全?
  - 否,可用 Collections.synchronizedSet 包装或使用并发容器。

### LinkedHashSet

`LinkedHashSet` 是 `HashSet` 的有序扩展,基于 `LinkedHashMap` 实现,通过哈希表与双向链表的组合,在保证元素唯一性和高效去重性能(插入、查询平均
O (1) 复杂度)的同时,默认按元素插入顺序维护遍历顺序,也可通过参数配置为访问顺序(最近访问的元素移至链表尾部)。其底层将元素作为 `LinkedHashMap` 的键存储,值统一使用占位符,双向链表节点记录前驱和后继关系,例如依次插入
“B”“A”“C” 时,遍历时会严格按 “B→A→C” 的顺序返回,而非哈希表的存储顺序。这种实现方式在插入时需额外维护链表指针(导致略高的内存占用和少许性能损耗),但换来了可预测的有序遍历,适用于日志去重(保留时间顺序)、LRU
缓存(结合访问顺序)、有序集合运算等场景,是需要有序性但无需排序逻辑时的理想选择,兼顾了 HashSet 的高效性与链表的顺序特性。

**常见问法:**

- LinkedHashSet 如何保证元素有序?

  - 通过双向链表维护元素的插入顺序(默认)或访问顺序(需显式配置),遍历时直接沿链表顺序访问,与元素在哈希表中的存储位置无关。
- LinkedHashSet 的插入性能比 HashSet 慢多少?

  - 理论上略慢,因需额外操作双向链表(如更新前驱 / 后继指针),但实际差异通常可忽略不计(尤其在数据量不大时)。
- LinkedHashSet 是否允许 null 元素?

  - 允许,与 HashSet 一致,最多存储一个 null 元素。
- 如何启用访问顺序模式?

  - 无法直接通过 LinkedHashSet 的构造函数配置,需通过 LinkedHashMap 间接实现:

```java
    LinkedHashMap<String, Object> map = new LinkedHashMap<>(16, 0.75f, true);
    Set<String> accessOrderedSet = map.keySet(); // 访问顺序的 Set
    ```
- LinkedHashSet 的遍历性能为何优于 TreeSet?

  - TreeSet 基于红黑树,遍历时需中序遍历(时间复杂度 O (n)),而 LinkedHashSet 直接沿双向链表顺序访问,无需排序操作,效率更高。
- LinkedHashSet 与 LinkedHashMap 的区别?

  - LinkedHashSet 是 Set 接口的实现,仅存储键,值统一为占位符;LinkedHashMap 是 Map 接口的实现,存储键值对,支持按插入
    / 访问顺序维护。
- 什么场景下应选择 LinkedHashSet 而非 TreeSet?

  - 仅需保留元素插入顺序,无需排序(如日志去重)。
  - 需频繁遍历集合,且期望稳定的 O (n) 时间复杂度。
  - LinkedHashSet 的线程安全版本如何实现?

    与 HashSet 类似,使用 `Collections.synchronizedSet()` 包装:

```java
    Set<String> safeSet = Collections.synchronizedSet(new LinkedHashSet<>());
    ```
- LinkedHashSet 的内存占用比 HashSet 高多少?

  - 每个元素需额外存储两个指针(前驱 / 后继),内存开销约增加 50%(视元素本身大小而定)。
- 如何利用 LinkedHashSet 实现 LRU 缓存?

  - 通过 LinkedHashMap 的访问顺序模式结合容量限制:

```java
    LinkedHashMap<K, V> cache = new LinkedHashMap<K, V>(capacity, 0.75f, true) {
        @Override
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
            return size() > capacity; // 超出容量时淘汰最旧元素
        }
    };
    ```

### TreeSet

`TreeSet` 是 Java 集合框架中基于红黑树实现的有序集合,作为 `TreeMap` 的键视图,它将元素存储为键、值统一为静态占位符,其去重逻辑与 `HashSet`、`LinkedHashSet` 不同,不依赖元素的 `equals()` 和 `hashCode()`,而是通过自然排序(元素实现 `Comparable` 接口)或定制排序(传入 `Comparator` 接口)维护元素顺序
—— 当元素通过 `compareTo()` 或 `Comparator` 比较结果为 0 时即视为重复元素,即便 `equals()` 返回
false 也不会存储(尽管如此,为避免逻辑混乱,通常建议保证当 a.compareTo (b) == 0 时 a.equals (b) 为
true)。

这种基于红黑树的实现使得插入、删除、查询操作的时间复杂度均为 O (log n),并提供了 subSet、lower、higher 等高效范围查询方法,非常适用于需要排序或区间筛选的场景,例如数值统计、年龄分段查询等。

不过,TreeSet 要求元素必须可比较(实现 Comparable 或提供 Comparator),且不支持存储 null 值,同时由于红黑树节点需要额外存储父节点、左右子节点及颜色标记,其内存占用要高于基于哈希表的
HashSet 和 LinkedHashSet。

**常见问法:**

- TreeSet 如何实现排序?
  - 自然排序:元素需实现 `Comparable` 接口(如 `Integer`、`String`),按 `compareTo()` 方法排序。
  - 定制排序:通过构造函数传入 `Comparator` 接口实现自定义排序逻辑(如逆序、按属性排序)。
  - 注意:若元素未实现 `Comparable` 且未指定 `Comparator`,插入时会抛出 `ClassCastException`。
- TreeSet 是否允许重复元素?
  - 逻辑去重:当两个元素通过 `compareTo()`(自然排序)或 `Comparator`(定制排序)比较结果为 `0` 时,视为
    “重复”,仅保留一个。
  - 与 `equals()` 无关:即使 `equals()` 返回 `false`,只要比较结果为 `0`,仍视为重复(可能导致
    “逻辑重复” 问题)。
- TreeSet 是否允许 `null` 值?
  - 不允许:插入 `null` 会抛出 `NullPointerException`(红黑树的比较逻辑不支持 `null`)。
  - 对比 `HashSet`:`HashSet` 允许单个 `null`,而 `TreeSet` 完全不支持。
- TreeSet 的底层数据结构是什么?
  - 基于 红黑树(自平衡二叉搜索树)实现,通过颜色标记和旋转操作保持平衡,确保插入、删除、查询的时间复杂度均为 O(log n)。
  - 内部通过 `TreeMap` 存储元素,元素作为 `Key`,`Value` 统一为静态占位符 `PRESENT`。
- TreeSet 如何实现范围查询?
  - 提供 `NavigableSet` 接口的范围操作方法:
    - `subSet(from, to)`:返回 [from, to) 区间的子集(左闭右开)。
    - `headSet(to, inclusive)`:返回小于等于 `to` 的元素(`inclusive` 为 `true` 时包含 `to`)。
    - `tailSet(from, inclusive)`:返回大于等于 `from` 的元素(`inclusive` 为 `true` 时包含 `from`)。
  - 边界查询方法:`lower()`、`higher()`、`floor()`、`ceiling()` 等,用于快速定位相邻元素。
- TreeSet 的线程安全性如何?
  - 非线程安全:多线程环境下并发操作可能导致数据不一致。
  - 解决方案:使用 `Collections.synchronizedSortedSet(new TreeSet<>())` 包装,手动添加同步控制。
- 自定义类如何使用 TreeSet?
  - 方案 1:类实现 `Comparable` 接口,重写 `compareTo()` 方法。

```java
    class User implements Comparable<User> {
        private int age;
        @Override
        public int compareTo(User o) {
            return Integer.compare(age, o.age); // 按年龄排序
        }
    }
    ```
  - 方案 2:创建 `TreeSet` 时传入 `Comparator` 匿名内部类或 lambda。

```java
    TreeSet<User> set = new TreeSet<>((u1, u2) -> u2.getAge() - u1.getAge()); // 逆序排序
    ```
- TreeSet 的性能如何?
  - 优点:排序和范围查询高效(O (log n)),适用于需要有序性和区间操作的场景。
  - 缺点:空间开销高于 `HashSet`(需存储树节点指针和颜色标记),插入 / 查询性能略低于哈希表。
- 如何避免 TreeSet 中的逻辑重复问题?
  - 确保元素的 `compareTo()`(或 `Comparator`)与 `equals()` 逻辑一致:
    - 若 `a.compareTo(b) == 0`,则 `a.equals(b)` 应返回 `true`,否则可能导致集合中存在
      “相等但不重复” 的元素。

### 对比

1. 无序 vs 有序:

   - `HashSet` 完全无序,适用于仅需去重的场景。
   - `LinkedHashSet` 按插入顺序维护顺序,适合需要保留操作顺序的场景(如用户操作记录去重)。
   - `TreeSet` 按排序规则(自然 / 定制)维护顺序,适合需要排序或范围查询的场景(如数值统计、年龄区间筛选)。
2. 性能与数据结构:

   - `HashSet` 基于哈希表,平均性能最优,适合高频插入 / 查询。
   - `LinkedHashSet` 因维护双向链表,内存和插入性能略低,但遍历顺序稳定。
   - `TreeSet` 基于红黑树,排序和范围查询高效,但空间开销大,单元素操作性能略逊于哈希表。
3. 特殊限制:

   - `TreeSet` 不支持 `null`,且元素必须可比较(`Comparable` 或 `Comparator`)。
   - `HashSet` 和 `LinkedHashSet` 允许 `null`,但仅能存储一个。
4. 选择
   - 优先选 HashSet:若无顺序或排序需求,仅需高效去重。
   - 需要插入顺序:选择 `LinkedHashSet`(如接口调用日志去重,保留请求顺序)。
   - 需要排序或范围查询:选择 `TreeSet`(如学生成绩排名、时间区间筛选)。
   - 多线程场景:所有 `Set` 实现均需手动同步(如 `Collections.synchronizedSet()` 或并发容器)。

## Queue / Deque

在 Java 中,<a href="/vault/2-Learning/04-Java/02-Java%E5%AE%B9%E5%99%A8/03-Queue%E7%B3%BB%E5%88%97/00-Queue%E5%92%8CDeque%E6%80%BB%E8%A7%88">Queue 和 Deque</a> 是两个非常重要的 **线性容器接口**,主要用于模拟 **队列**、**双端队列** 和 **栈** 等数据结构,常见于消息队列、缓冲区、线程调度等场景。

它们位于 `java.util` 包中:

- `Queue<E>`:先进先出(FIFO),通常用于排队模型(如线程池、任务调度)
- `Deque<E>`:双端队列(Double-Ended Queue),支持头尾两端插入/删除,可模拟队列、栈等多种结构

```java
Queue(接口)   ← FIFO(队列)

├── LinkedList           // 经典实现,基于链表,功能全面(支持 List、Deque)
├── PriorityQueue        // 优先级队列,基于最小堆
├── ArrayDeque           // 高性能双端队列,基于循环数组

Deque(接口)    ← 双端队列(可作为队列 + 栈)

├── LinkedList           // 实现了 Deque 接口,支持头尾操作
├── ArrayDeque           // 数组实现,性能更优,推荐使用

ArrayDeque

Java 中ArrayDeque 是基于循环数组实现的双端队列,通过 headtail 指针标记队列头尾,利用位运算(index & (length-1))(代替取模运算)实现循环索引(要求容量为 2 的幂),支持高效的双端插入 / 删除操作(均摊 O(1) 时间复杂度)。当 head == tail 且数组已满时触发扩容,新容量为原数组的 2 倍,并通过双段复制(先复制 head 到队尾,再复制数组头部到 tail 位置)保持元素顺序。其底层数组连续存储,缓存局部性好,内存占用比链表更紧凑,性能优于 LinkedList,但不允许存储 null(避免与 poll 返回 null 混淆),且非线程安全。适用于实现栈(LIFO)、队列(FIFO)、双端队列及滑动窗口等场景,是替代 LinkedList 的高效选择。

常见问法:

  • ArrayDeque 的底层结构是什么?与 LinkedList 有什么区别?

    • ArrayDeque 是基于循环数组实现的,插入删除通过数组偏移完成;LinkedList 是基于双向链表。前者更高效,内存更紧凑。
  • ArrayDeque 是如何扩容的?触发条件是什么?

    • head == tail,即容量不足时触发扩容,策略是扩大为原数组的 2 倍,使用双段拷贝保留元素顺序。
  • 为什么不允许存储 null 元素?

    • 为避免与 poll()peek() 返回 null 混淆(无法判断队列为空还是存储了 null)。
  • ArrayDeque 适合作为栈使用吗?

    • 适合,且比 Stack 更高效(Stack 是线程安全的,基于向量实现,性能较低)。例如:
    ArrayDeque<Integer> stack = new ArrayDeque<>();
    stack.push(1); // 入栈
    int top = stack.pop(); // 出栈
    ```
- ArrayDeque 如何实现双端队列?

  - 支持从头部(`offerFirst`/`pollFirst`)和尾部(`offerLast`/`pollLast`)插入
    / 删除元素,满足双端操作需求。
- ArrayDeque 是线程安全的吗?如何在并发场景下使用?
  - 非线程安全,可使用 `Collections.synchronizedDeque(new ArrayDeque<>())` 包装,或直接使用 `ConcurrentLinkedDeque`。
- ArrayDeque 适合哪些场景?
  - 适用于实现栈、队列、双端队列、缓存、滑动窗口等,尤其适合高性能场景中替代 `LinkedList`。

### PriorityQueue

Java 中的 `PriorityQueue` 是基于最小堆(完全二叉树)实现的优先级队列,底层通过动态数组 `Object[] queue` 存储元素,利用数组下标计算父子节点关系(左子节点为 `2i+1`,右子节点为 `2i+2`,父节点为 `(i-1)/2`),默认构建最小堆(元素按自然顺序升序排列),也可通过传入 `Comparator` 自定义排序规则(如降序)。插入元素时通过 `siftUp` 向上堆化维护堆序(时间复杂度
O(log n)),删除堆顶元素时通过 `siftDown` 向下堆化调整(时间复杂度 O(log n)),访问堆顶元素(peek)则为
O(1)。其扩容策略为小数组(初始容量 11,容量 <64 时)翻倍、大数组按 1.5 倍扩容,通过 `Arrays.copyOf` 复制元素(耗时
O(n))。该队列不允许存储 null,非线程安全,遍历时按数组顺序而非堆序,适用于任务调度、Top-K 问题等需要动态维护优先级的场景。

**常见问题:**

- PriorityQueue 是如何实现优先级排序的?

  - 基于最小堆(完全二叉树),元素通过自然排序(`Comparable`)或自定义比较器(`Comparator`)确定优先级,堆顶始终为优先级最高的元素(默认最小值)。
- PriorityQueue 是如何维护顺序的?

  - 插入时 `siftUp()`,删除堆顶时 `siftDown()`,维护最小堆性质。
- 添加/删除的时间复杂度是多少?

  - O(log n),通过堆化完成。
- 底层数据结构是什么?

  - 动态数组(Object[] queue),构成完全二叉堆。
- PriorityQueue 允许重复元素吗?

  - 允许,队列中可以存储重复元素,仅根据优先级排序,不涉及去重逻辑。
- null 能作为元素加入吗?
  - 不允许插入 null,会抛 NullPointerException。
- 为什么 PriorityQueue 不允许 null?

  - `poll()` 方法在队列为空时返回 `null`,若允许存储 `null` 会导致无法区分
    “队列为空” 和 “存储了 `null` 元素”。
- PriorityQueue 的遍历顺序是怎样的?

  - 遍历顺序与优先级无关,仅按底层数组的存储顺序(非排序顺序),若需有序输出需反复调用 `poll()`。
- 遍历 PriorityQueue 会按顺序输出吗?
  - 否,遍历不保证顺序,只有反复 poll() 才是从小到大输出。
- PriorityQueue 是线程安全的吗?

  - 非线程安全,多线程环境下需手动同步(如使用 `Collections.synchronizedQueue()`)或改用 `PriorityBlockingQueue`。
- 如何实现最大堆?

  - 通过自定义比较器实现降序,例如:

```java
    PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
    ```
- 扩容时的性能开销如何?

  - 扩容时需复制原数组元素(`Arrays.copyOf`),时间复杂度为 O(n),因此预估元素数量后设置初始容量可减少扩容次数。

### LinkedList

见List

### 对比

1. 数据结构
   - ArrayDeque:循环数组(动态扩容,双端指针)。
   - PriorityQueue:最小堆(动态数组,堆化维护顺序)。
   - LinkedList:双向链表(节点含前驱 / 后继指针)。
2. 顺序与排序
   - ArrayDeque:按插入顺序(双端有序)。
   - PriorityQueue:按优先级排序(自然 / 自定义比较器)。
   - LinkedList:按插入顺序(有序列表,无自动排序)。
3. 性能
   - | 操作 | ArrayDeque | PriorityQueue | LinkedList |
     | --- | --- | --- | --- |
     | 两端增删 | O (1)(最快) | 不支持 | O(1) |
     | 随机访问 | O(1) | 不支持 | O (n)(最慢) |
     | 中间增删 | O(n) | 不支持 | O(1) |
     | 内存占用 | 低(数组紧凑) | 中(堆结构) | 高(指针开销) |
4. 特殊限制
   - ArrayDeque:不允许 `null`,容量为 2 的幂。
   - PriorityQueue:不允许 `null`,元素需可比较(`Comparable`/`Comparator`)。
   - LinkedList:允许 `null`,随机访问低效。
5. 适用场景
   - ArrayDeque:栈、队列、滑动窗口(双端操作优先)。
   - PriorityQueue:任务调度、Top-K、优先级排序(需动态维护极值)。
   - LinkedList:纯链表操作、头尾频繁增删(如消息队列,无需随机访问)。

双端操作选 ArrayDeque(性能最高),优先级排序选 PriorityQueue(唯一选择),链表场景选 LinkedList(迫不得已时)。

## Map

<a href="/vault/2-Learning/04-Java/02-Java%E5%AE%B9%E5%99%A8/04-Map%E7%B3%BB%E5%88%97/00-Map%E6%80%BB%E8%A7%88">Map</a> 是 Java 集合框架中的核心接口之一,用于存储键值对(Key-Value
Pairs),特点是通过键(Key)快速查找值(Value),类似于现实中的字典。与 Set(存储单一元素)和 Queue(线性容器)不同,Map
专注于映射关系的维护,广泛应用于缓存、配置表、统计计数等场景。

### HashMap<K, V>

`HashMap` 是 Java 中最常用的键值对映射容器,底层采用 **数组 + 链表 + 红黑树** 的复合结构实现,其中数组用于定位桶位(Bucket),链表或红黑树用于解决哈希冲突。JDK
1.8 之前,冲突元素以链表形式存在;JDK 1.8 开始,当链表长度 ≥ 8 且数组容量 ≥ 64 时,链表会转为红黑树,提升查询性能(O(n)
→ O(log n))。

每次 put 操作流程是:先通过 `hashCode()` 计算 key 的哈希值,并经过扰动函数优化后确定桶下标(`hash & (table.length - 1)`),然后判断该位置是否已有元素:

- 若无,直接插入;
- 若有,比较 key 是否相同,相同则覆盖,不同则采用尾插链表或插入红黑树。

当元素数量超过阈值(默认容量 × 0.75),会触发 **扩容**:新数组容量为原来的 2 倍,并重新哈希已有元素的位置,这一步称为
rehash,是最耗性能的操作。

HashMap 允许一个 `null` 键和多个 `null` 值,不保证元素顺序,且 **非线程安全**,多线程场景下需使用 `ConcurrentHashMap` 或手动同步。

使用 HashMap 时,应合理设置初始容量与负载因子,避免频繁扩容带来的性能开销;同时,若使用自定义类作为 key,必须同时重写 `equals()` 和 `hashCode()` 方法,以确保哈希一致性和键的唯一性。

总结来说,HashMap 提供了插入、查找、删除操作平均 O(1) 的性能,是构建缓存、索引、字典等数据结构的首选工具,但在并发和扩容时需特别注意其设计边界和使用规范。

**常见问法:**

- HashMap 的底层数据结构是什么?
  - 数组 + 链表 + 红黑树(JDK 1.8 起链表转红黑树)
- HashMap 如何解决哈希冲突?
  - 通过链地址法(链表/红黑树)解决,key 相同通过 equals() 判断。
- 什么时候会将链表转为红黑树?
  - 桶中链表长度 ≥ 8 且数组容量 ≥ 64 时。
- HashMap 的 put 操作是怎么执行的?
  - 计算 hash → 定位桶位 → 冲突处理(覆盖/插入) → 超过阈值扩容。
- 为什么 HashMap 的数组长度必须是 2 的幂?
  - 为了用 `hash & (length - 1)` 替代 `%` 运算,提高效率。
- HashMap 的默认初始容量是多少?负载因子默认多少?
  - 默认初始容量为 16,默认负载因子为 0.75。
- HashMap 的扩容机制是怎样的?
  - 元素个数 > 容量 × 负载因子时,扩容为原容量的 2 倍,并重新散列。
- 为什么扩容性能开销大?
  - 需要重新分配数组,并将所有元素重新计算 hash 和插入新桶。
- 如何优化 HashMap 的性能?
  - 预估容量,合理设置初始容量,减少扩容;确保 key 的 hash 分布均匀。
- HashMap 允许 null 吗?
  - 允许一个 null 键,允许多个 null 值。
- 为什么必须重写 hashCode() 和 equals()?
  - 保证 key 唯一性和哈希一致性,否则会导致重复/找不到等问题。
- 只重写了 equals() 或只重写 hashCode() 会有什么问题?
  - 破坏哈希一致性,相同对象可能落入不同桶,无法正确查找或去重。
- HashMap 和 Hashtable 有什么区别?
  - HashMap 非线程安全,效率高;Hashtable 线程安全但性能低,是遗留类。
- HashMap 和 ConcurrentHashMap 有什么区别?
  - 后者线程安全,基于分段锁(JDK 7)或 CAS + synchronized(JDK 8),支持并发操作。
- HashMap 和 TreeMap 的区别?
  - TreeMap 基于红黑树,有序;HashMap 无序,性能更高。
- HashMap 和 LinkedHashMap 的区别?
  - LinkedHashMap 维护插入顺序或访问顺序(可实现 LRU 缓存)。
- HashMap 是线程安全的吗?
  - 否,多线程环境中使用会导致数据丢失、死循环等问题。
- JDK 1.7 中 HashMap 为什么可能死循环?
  - 链表头插 + 多线程扩容时可能形成环形链表,导致死循环。
- JDK 1.8 中为什么不会再死循环?
  - 改用链表尾插法 + 更安全的扩容方式。
- 多线程下如何安全使用 HashMap?
  - 用 `ConcurrentHashMap` 或 `Collections.synchronizedMap()`。
- HashMap 的 key 可以变吗?
  - 不推荐,key 一旦参与哈希运算再修改其值,可能导致查找失败。
- 如何遍历 HashMap?有几种方式?
  - 可通过 keySet()、entrySet()、values()、forEach()、迭代器等方式遍历。
- 遍历 HashMap 的顺序是固定的吗?
  - 否,不保证顺序;如需顺序应使用 LinkedHashMap 或 TreeMap。

### LinkedHashMap<K, V>

`LinkedHashMap` 是 Java 集合框架中 `HashMap` 的子类,它通过**哈希表 + 双向链表**的复合结构,在保持 `HashMap` 快速查找特性(平均
O (1) 时间复杂度)的基础上,提供了**插入顺序**或**访问顺序**的遍历能力。与 `HashMap` 不同,`LinkedHashMap` 的每个节点(Entry)包含额外的`before`和`after`指针,用于维护双向链表,从而实现顺序控制。默认按插入顺序遍历,元素会按插入顺序依次排列;若设置`accessOrder=true`(如`new LinkedHashMap<>(capacity, loadFactor, true)`),则每次访问(如`get`或`put`)会将元素移至链表尾部,形成**LRU(最近最少使用)顺序**。这种特性使其特别适合实现缓存,例如通过继承
LinkedHashMap 并重写`removeEldestEntry`方法,可以自动淘汰最旧元素:

```java
class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;
    public LRUCache(int capacity) {
        super(capacity, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
                return size() > capacity;
            }
        };
    }
}

LinkedHashMap 的遍历效率稳定为 O (n),优于 HashMap 的无序遍历(依赖哈希分布),但每个节点需额外存储两个指针,空间开销略高。它允许一个 null 键和多个 null 值,且非线程安全,多线程场景需通过Collections.synchronizedMap包装或改用 ConcurrentHashMap。与 TreeMap 相比,LinkedHashMap 维护的是插入 / 访问顺序,而非键的自然排序,适用于需要保留元素顺序的场景(如日志记录、JSON 字段顺序输出)。使用时需注意,若键为自定义类,需重写equals()hashCode()方法,且避免修改键对象的属性,以免破坏哈希一致性。

常见问题:

  • LinkedHashMap 如何保证元素有序?

    • 通过双向链表维护元素顺序,分为两种模式:
      • 插入顺序(默认):按元素插入的先后顺序排序。
      • 访问顺序(accessOrder=true):每次访问(get/put)后将元素移至链表末尾,适用于 LRU 缓存。
  • LinkedHashMap 的底层数据结构是什么?

    • 继承自 HashMap,底层为哈希表(数组 + 链表 / 红黑树)+ 双向链表:
      • 哈希表用于快速定位元素;
      • 双向链表用于记录元素顺序(每个节点包含 beforeafter 指针)。
  • LinkedHashMap 与 HashMap 的主要区别是什么?

    • 顺序性:LinkedHashMap 支持插入顺序或访问顺序,HashMap 完全无序。
    • 数据结构:LinkedHashMap 额外维护双向链表,HashMap 无。
    • 内存开销:LinkedHashMap 每个节点多两个指针,空间占用略高。
  • 如何实现 LRU 缓存?

    • 通过 accessOrder=true 开启访问顺序,并重写 removeEldestEntry 方法淘汰最旧元素:
    new LinkedHashMap<K, V>(capacity, 0.75f, true) {
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
            return size() > capacity; // 超出容量时删除最旧元素
        }
    };
    ```
- LinkedHashMap 是否允许 null 键值?

  - 允许1null 键和多个 null 值,与 `HashMap` 一致。
- 遍历顺序是固定的吗?

  - 是。插入顺序模式下,遍历顺序与插入顺序一致;访问顺序模式下,遍历顺序随访问动态调整(最近访问的元素在链表末尾)。
- LinkedHashMap 是线程安全的吗?如何处理多线程场景?

  - 非线程安全。多线程下可通过 `Collections.synchronizedMap(new LinkedHashMap<>())` 包装,或改用线程安全的 `ConcurrentHashMap`(但 `ConcurrentHashMap` 不保证顺序)。
- 为什么遍历 LinkedHashMap 比 HashMap 更高效?

  - HashMap 遍历需遍历哈希表桶和链表 / 红黑树,顺序不可预测;
  - LinkedHashMap 直接按双向链表顺序遍历,时间复杂度稳定为 O(n)。
- LinkedHashMap 如何维护双向链表?

  - 插入元素时,通过 `linkNodeLast` 方法将新节点追加到链表末尾(插入顺序)或调整到末尾(访问顺序)。
  - 删除元素时,更新相邻节点的 `before` 和 `after` 指针,保持链表连续。
- accessOrder 参数的作用是什么?

  - `accessOrder=false`(默认):按插入顺序排序;
  - `accessOrder=true`:按访问顺序排序,每次访问元素会将其移至链表末尾,实现 LRU 逻辑。
- LinkedHashMap 的迭代器是如何工作的?

  - 直接按双向链表顺序遍历(从 `head` 到 `tail`),无需访问哈希表桶,保证顺序稳定。
- LinkedHashMap 的插入性能比 HashMap 慢多少?

  - 略慢,因需额外操作双向链表指针(如更新 `before`/`after`),但实际差异通常可忽略(尤其数据量较小时)。
- 如何优化 LinkedHashMap 的内存占用?

  - 避免存储大量小对象(指针开销占比更高);
  - 按需选择顺序模式,避免不必要的链表调整(如仅需插入顺序时,不启用 `accessOrder`)。

### TreeMap<K, V>

`TreeMap` 是 Java 中基于 红黑树(自平衡二叉搜索树) 实现的有序映射,键值对按键的自然顺序(如整数升序、字符串字典序)或自定义比较器(`Comparator`)
排序。其底层通过红黑树节点维护元素顺序,每个节点包含键、值、颜色标记(红 / 黑)及父、左、右子节点指针,确保中序遍历结果为有序序列(升序)。`TreeMap` 支持高效的范围查询(如 `subMap`、`headMap`)和有序操作(如获取第一个
/ 最后一个键),插入、删除、查询的时间复杂度均为 O(log n),但不支持 null 键(无法参与比较),且非线程安全。典型应用包括数值范围统计、按时间排序的日志记录等需要有序性的场景。

### Hashtable<K, V>

`Hashtable` 是 Java 1.0 时代遗留的哈希表实现,可视为"线程安全版的古老 HashMap":

| 维度 | Hashtable | HashMap |
| --- | --- | --- |
| 线程安全 | ✅ 方法级 `synchronized`(粗粒度锁,读读也互斥) | ❌ 非线程安全 |
| null 支持 | 键、值都**不允许** null(直接抛 NPE) | 允许 1null 键、多个 null 值 |
| 继承体系 | 继承老类 `Dictionary` | 继承 `AbstractMap` |
| 初始容量 / 扩容 | 11 / 2n+1 | 16 / 2n(保持 2 的幂,利于位运算取模) |
| 定位 | 遗留类,新代码不使用 | 默认首选 |

两点面试要点:一是 **null 红线**——`Hashtable` 键值都拒绝 null,原因在于它为多线程环境设计:`get(key)` 返回 null 时无法区分"键不存在""值就是 null"(`HashMap` 单线程场景可用 `containsKey` 消歧);二是它和 `Vector` 同病相怜——方法级 synchronized 读读互斥,如今被 `ConcurrentHashMap`(JDK 8 后 CAS+细粒度锁)全面替代。

## 全景选型决策图

```mermaid
flowchart TD
    Q{"要装什么数据?"} -->|"键值对(映射)"| M{"需要排序吗?"}
    Q -->|"单个元素"| C{"元素要去重吗?"}
    C -->|"要(唯一)"| S{"需要保持顺序?"}
    C -->|"不要(可重复)"| L{"操作集中在哪?"}
    L -->|"随机访问、读多"| AL["ArrayList"]
    L -->|"头尾增删、当队列/栈"| LL["LinkedList(或 ArrayDeque)"]
    S -->|"不要顺序"| HS["HashSet"]
    S -->|"插入顺序"| LHS["LinkedHashSet"]
    S -->|"按值排序"| TS["TreeSet"]
    M -->|"不需要"| HM["HashMap(默认选它)"]
    M -->|"按插入/访问顺序"| LHM["LinkedHashMap(LRU)"]
    M -->|"按键排序"| TM["TreeMap"]
    Q2{"多线程?"} -.->|是| CONC["HashMap→ConcurrentHashMap<br/>List→CopyOnWriteArrayList<br/>Queue→ConcurrentLinkedQueue / BlockingQueue"]

选型三问:装什么形态(单值 or 键值对)→ 要什么顺序(无序 / 插入序 / 排序)→ 在什么环境(单线程随手用 / 多线程选并发包)。所有实现类的深读见各系列详篇。


⬅️ 00-Java 🏠 本页是 02-Java容器 入口 ➡️ 00-List总览