【Java 集合】核心知识点梳理
Java 集合框架核心知识点梳理
一、集合框架整体概览
Java 集合是用于存储、管理和操作一组对象的容器,核心作用是替代数组的局限性,提供更灵活、高效的数据存储方案。
1. 集合与数组的核心区别
特性 | 数组 | 集合 |
---|---|---|
长度特性 | 固定长度,创建后无法修改 | 动态长度,支持自动扩容 / 缩容 |
存储类型 | 可存储基本数据类型(int、long 等)或对象 | 仅存储对象(基本类型需自动装箱为包装类) |
访问方式 | 直接通过下标访问(O (1) 时间复杂度) | 需通过迭代器、遍历或键(Map)访问 |
功能丰富度 | 仅提供长度属性,无额外操作方法 | 内置增删改查、排序、过滤等丰富方法(如add() 、remove() 、contains() ) |
适用场景 | 存储固定数量、类型统一的数据 | 存储数量不确定、需频繁操作(增删改查)的数据 |
2. 集合框架体系结构
Java 集合框架核心分为两大体系,基于Collection
和Map
接口:
3. 常用集合总结
日常开发中高频使用的集合:
- List:ArrayList(查询优先)、LinkedList(增删优先);
- Set:HashSet(去重 + 高效查询)、LinkedHashSet(去重 + 有序);
- Map:HashMap(键值对存储,高频首选)、LinkedHashMap(键值对 + 有序)、ConcurrentHashMap(多线程场景)。
二、List 接口核心实现类
List 接口的核心特点是有序(元素插入顺序与遍历顺序一致)、可重复,重点掌握 ArrayList 和 LinkedList。
1. ArrayList(动态数组)
(1)底层原理
- 底层基于
Object[]
数组实现,默认初始容量为 10; - 实现
List
接口,支持随机访问(通过下标直接获取元素),查询效率极高; - 非线程安全,多线程环境需手动同步(如
Collections.synchronizedList()
)。
(2)扩容机制
- 触发条件:当元素数量(size)超过当前数组容量时,触发扩容;
- 扩容公式:新容量 = 旧容量 + 旧容量右移 1 位(等价于
newCapacity = oldCapacity * 1.5
);- 例:初始容量 10,插入第 11 个元素时,扩容为 15;再满则扩容为 22(15*1.5);
- 扩容流程:创建新容量的数组 → 复制旧数组元素到新数组 → 指向新数组(时间复杂度 O (n),扩容频繁会影响性能)。
(3)面试重点:为什么扩容因子是 1.5?
- 平衡时间与空间效率:
- 扩容因子过小(如 1.2):频繁扩容,复制元素的开销增大,时间复杂度退化;
- 扩容因子过大(如 2.0):空间浪费严重(可能有大量空闲数组位置);
- 高效计算:1.5 可通过 “右移 1 位 + 加法” 实现(
oldCapacity + (oldCapacity >> 1)
),避免浮点数运算,提升执行效率; - 内存利用率:基于统计规律,1.5 能保证数组的空闲率适中,减少内存碎片。
(4)核心性能特点
- 优势:随机访问(
get(int index)
)效率高(O (1)); - 劣势:增删元素效率低(尤其是中间位置)—— 需移动数组元素(O (n))。
2. LinkedList(双向链表)
(1)底层原理
- 底层基于双向链表实现,每个节点(Node)包含
prev
(前驱指针)、item
(元素)、next
(后继指针); - 实现
List
和Queue
接口,支持队列的先进先出(FIFO)操作; - 非线程安全。
(2)核心性能特点
- 优势:头尾增删元素(
addFirst()
、addLast()
、removeFirst()
)效率高(O (1)); - 劣势:随机访问效率低(O (n))—— 需从链表头 / 尾遍历到目标位置。
3. ArrayList vs LinkedList 对比(面试高频)
对比维度 | ArrayList | LinkedList |
---|---|---|
底层结构 | 动态数组 | 双向链表 |
随机访问 | O (1)(高效) | O (n)(低效) |
中间增删 | O (n)(需移动元素) | O (n)(需遍历找到位置) |
头尾增删 | O (n)(需扩容 / 移动元素) | O (1)(高效) |
内存占用 | 连续内存,少量空闲空间(扩容预留) | 非连续内存,每个节点额外存储指针(内存开销略大) |
适用场景 | 查询频繁、增删少的场景(如数据展示) | 增删频繁(尤其是头尾)、查询少的场景(如队列、栈) |
三、Map 接口核心实现类
Map 接口的核心特点是存储键值对 <K,V>、键唯一(重复键会覆盖值)、值可重复,重点掌握 HashMap、LinkedHashMap 和 ConcurrentHashMap。
1. HashMap(核心重点)
(1)底层原理(JDK1.7 vs JDK1.8)
JDK 版本 | 底层结构 | 核心差异 |
---|---|---|
1.7 | 数组 + 链表 | 扩容时采用头插法,多线程下易产生环形链表(死循环) |
1.8 | 数组 + 链表 + 红黑树 | 扩容时采用尾插法,解决死循环;链表长度≥8 且数组长度≥64 时,链表转为红黑树(查询效率从 O (n)→O (logn)) |
(2)核心参数
- 默认初始容量:16(必须是 2 的次幂);
- 负载因子:0.75(阈值 = 容量 × 负载因子,默认初始阈值 = 16×0.75=12);
- 树化阈值:链表长度≥8;
- 反树化阈值:红黑树节点数≤6(还原为链表,降低树维护成本)。
(3)put () 方法核心流程(JDK1.8)
- 哈希计算与槽位定位 :
- 扰动计算:
h = key.hashCode() ^ (h >>> 16)
(将哈希值高位特征融入低位,减少冲突); - 若数组未初始化,调用
resize()
初始化(默认容量 16); - 槽位计算:
index = (n-1) & h
(n 为数组长度,2 的次幂保证 n-1 二进制全 1,让哈希值充分参与运算)。
- 扰动计算:
- 处理目标桶元素:
- 桶为空:直接新建 Node 节点存入,
modCount
(修改次数)和size
递增,进入扩容检查; - 桶非空(哈希冲突):
- 检查桶中第一个节点:若 key 的 hash 和 equals 匹配,标记为待更新节点;
- 不匹配则分情况:
- 节点是 TreeNode(已树化):调用
putTreeVal()
在红黑树中查找 / 新增节点; - 节点是普通链表:遍历链表,找到匹配节点则标记更新,否则新增节点挂在链尾,
modCount
和size
递增。
- 节点是 TreeNode(已树化):调用
- 桶为空:直接新建 Node 节点存入,
- 树化检查:新增链表节点后,若链表长度≥8 且数组长度≥64,调用
treeifyBin()
转为红黑树(数组长度不足 64 则优先扩容)。 - 更新操作:若找到匹配节点,用新 value 覆盖旧 value,返回旧 value(不触发扩容)。
- 扩容检查:若为新增节点,检查
size
是否超过阈值,超过则调用resize()
扩容。
(4)扩容机制(核心面试点)
- 触发条件:
size > 容量×负载因子
(新增元素后检查); - 扩容核心流程:
- 新容量 = 旧容量 ×2(保持 2 的次幂),新阈值 = 新容量 ×0.75;
- 新建 2 倍长度的数组,迁移旧数组中所有节点:
- 无需重新计算 hash,仅判断原哈希值中 “旧容量对应的 bit 位”:
- 该 bit 为 0:新索引 = 原索引;
- 该 bit 为 1:新索引 = 原索引 + 旧容量;
- 红黑树迁移后,若节点数≤6 则还原为链表。
- 无需重新计算 hash,仅判断原哈希值中 “旧容量对应的 bit 位”:
- 为什么扩容是 2 的次幂?
- 保证
(n-1) & h
运算等价于 “哈希值对 n 取模”,且运算效率更高(位运算比取模快); - n 为 2 的次幂时,n-1 二进制全 1,能让哈希值的每一位都参与运算,降低哈希冲突概率。
- 保证
(5)哈希冲突相关
- 定义:不同 key 的哈希值经计算后映射到同一个桶(数组下标),即为哈希冲突;
- 产生原因:哈希函数的输出空间有限(数组容量固定),而输入的 key 是无限的,必然存在 “多对一” 映射;
- 解决方法:
- 拉链法(HashMap 采用):每个桶维护一个链表 / 红黑树,冲突元素存入链表 / 树中;
- 再哈希法:使用多个哈希函数,若第一个函数冲突则用第二个计算;
- 扩容法:扩大数组容量,重新分配节点,减少冲突概率。
(6)多线程安全问题(JDK1.7 vs JDK1.8)
JDK1.7,多线程背景下,在数组扩容的时候,存在 Entry 链死循环和数据丢失问题,因为resize() 时使用头插法将旧链表节点迁移到新数组。
- JDK 1.7:并发扩容 → 头插法 → 环形链表 → 死循环
- 并发场景:
- 线程 A 和线程 B 同时触发扩容
- 假设链表中有节点:a → b → null
- 线程 A 执行到一半被挂起,此时已经将 a 迁移到新数组,形成 a → null
- 线程 B 完整执行扩容,由于头插法,新链表变为 b → a → null
- 线程 A 恢复执行,继续迁移 b,但此时 b.next 指向 a,而 a.next 在 B 线程中已经指向 null,但 A 线程仍按原链表迁移,可能导致 a.next = b,形成 b ⇄ a 的环形链表
- JDK1.8,解决了该问题,但是多线程背景下,put 方法存在数据覆盖的问题。
- JDK 1.8:并发插入 → 无锁判断 → 数据覆盖
- 尾插法替代头插法:扩容时保持链表顺序,避免环形链表
- 数据结构可升级为数组 + 红黑树(当链表长度 ≥ 8 且数组长度 ≥ 64)
- 遗留问题:数据覆盖
在A线程通过Key计算出hashcode,通过hashcode计算出数组bucket中的下标,判断桶中没有头结点Node,接着由于网络波动原因,CPU时间片耗尽了。与此同时,线程B也通过key算出hashcode(可能通过hash冲突碰撞到了一起),然后也找到了bucket下标,判断桶中也没有头结点Node,然后插入Value,紧接着,线程A再次获得时间片,插入Value,最终线程B,数据被覆盖了,没有考虑到并发安全性。
JDK 版本 | 核心问题 | 原因 |
---|---|---|
1.7 | 死循环 + 数据丢失 | 扩容时采用头插法,多线程并发迁移链表会导致环形链表 |
1.8 | 数据覆盖 | 无锁机制,多线程同时插入相同 key 或同一桶时,后插入的会覆盖先插入的 |
(7)解决多线程安全的方案
Collections.synchronizedMap(new HashMap<>())
:对 HashMap 加全局锁,性能低;Hashtable
:底层对所有方法加synchronized
,全局锁,性能差(已过时);ConcurrentHashMap
:高并发首选,采用分段锁(1.7)或 CAS+synchronized(1.8),锁粒度更细,性能高。
2. LinkedHashMap
- 底层原理:哈希表(HashMap)+ 双向链表,既保留 HashMap 的高效查询,又保证元素有序(插入顺序或访问顺序);
- 核心特性:
- 有序性:双向链表维护元素顺序,默认按插入顺序遍历,可通过构造函数指定 “访问顺序”(
accessOrder=true
,最近访问的元素排在尾部); - 应用场景:LRU 缓存(基于访问顺序淘汰最久未使用的元素);
- 有序性:双向链表维护元素顺序,默认按插入顺序遍历,可通过构造函数指定 “访问顺序”(
- 与 HashMap 的关系:继承 HashMap,重写了 Node 节点(增加
before
和after
指针),通过链表维护顺序。
3. ConcurrentHashMap(线程安全)
(1)JDK1.7 vs JDK1.8 实现差异
JDK 版本 | 底层结构 | 锁机制 | 核心优势 |
---|---|---|---|
1.7 | Segment 数组 + HashEntry 链表 | 分段锁(每个 Segment 是独立锁) | 支持并发度(默认 16),不同 Segment 可并行操作 |
1.8 | 数组 + 链表 + 红黑树 | CAS+synchronized(锁单个桶节点) | 锁粒度更细,性能更高;支持红黑树优化查询 |
(2)核心特点
- 线程安全:通过锁机制保证高并发下的数据一致性;
- 高效性:避免全局锁,支持多线程并行读写;
- 功能完善:支持
putIfAbsent()
(不存在则插入)、size()
(无锁统计)等并发安全方法。
四、Set 接口核心实现类
Set 接口的核心特点是无序、不可重复,其实现均依赖 Map 接口(用 Map 的 key 存储 Set 元素,value 为固定空对象)。
1. HashSet
- 底层原理:基于 HashMap 实现,value 是一个固定的
PRESENT
对象(private static final Object PRESENT = new Object()
); - 去重机制:依赖 HashMap 的 key 唯一性,即插入元素时:
- 计算元素的 hash 值,定位到对应的桶;
- 通过
hashCode()
和equals()
方法判断是否存在相同元素; - 存在则不插入,不存在则作为 key 存入 HashMap;
- 核心特点:无序(插入顺序与遍历顺序不一致)、高效(查询、插入、删除均为 O (1) 时间复杂度)、非线程安全。
2. LinkedHashSet
- 底层原理:基于 LinkedHashMap 实现,保留 LinkedHashMap 的有序性;
- 核心特点:不可重复 + 有序(插入顺序),性能略低于 HashSet,但有序场景下更适用;
- 与 HashSet 的区别:仅多了双向链表维护顺序,其他特性(去重机制、效率)与 HashSet 一致。
3. TreeSet
- 底层原理:基于 TreeMap 实现,key 通过红黑树排序;
- 核心特点:
- 有序性:默认按元素的自然顺序(如 String 字典序、Integer 数值序)排序,也可通过
Comparator
自定义排序; - 去重机制:通过排序规则判断元素是否重复(而非
hashCode()
和equals()
);
- 有序性:默认按元素的自然顺序(如 String 字典序、Integer 数值序)排序,也可通过
- 适用场景:需要有序且去重的场景(如排序后的用户 ID 集合)。
五、核心面试高频考点总结
1. 基础类对比
- ArrayList vs LinkedList:底层结构→性能差异→适用场景;
- HashMap vs Hashtable:线程安全→底层结构→扩容机制(HashMap 初始容量 16,Hashtable 初始容量 11;HashMap 负载因子 0.75,Hashtable 0.75);
- HashSet vs TreeSet:有序性→去重机制→性能。
2. 核心原理类问题
- 为什么 ArrayList 扩容因子是 1.5?
- 为什么 HashMap 的容量是 2 的次幂?
- HashMap 的哈希扰动函数有什么作用?
- HashMap 在 JDK1.8 为什么引入红黑树?
- HashSet 如何保证元素不重复?
3. 多线程相关
- HashMap 为什么是非线程安全的?JDK1.7 和 1.8 的问题差异?
- 多线程下如何安全使用 HashMap?
- ConcurrentHashMap 的锁机制演变?
4. 实践场景类
- 什么场景用 ArrayList?什么场景用 LinkedList?
- 如何实现一个 LRU 缓存?(LinkedHashMap 的访问顺序)
- 高并发场景下,键值对存储首选什么集合?(ConcurrentHashMap)
六、复习总结
Java 集合的核心是 “底层结构决定性能,场景决定选型”,复习时需重点掌握:
- 每个核心集合的底层结构(数组、链表、哈希表、红黑树);
- 关键机制(扩容、树化、去重、锁机制)的原理;
- 不同场景下的集合选型(查询优先→ArrayList,增删优先→LinkedList,键值对→HashMap,高并发→ConcurrentHashMap);
- JDK 版本差异(尤其是 HashMap 和 ConcurrentHashMap)。
建议结合 “原理 + 面试题” 的方式复习,比如通过 “HashMap 的 put 流程” 串联哈希计算、冲突处理、扩容、树化等多个知识点,同时动手写简单示例(如 ArrayList 扩容测试、HashMap 去重测试),加深理解。