目录
1.说说 Java 中 HashMap 的原理?
2.Java 中的 List 接口有哪些实现类?
3.Java 中 ConcurrentHashMap 1.7 和 1.8 之间有哪些区别?
4.为什么 JDK 1.8 对 HashMap 进行了红黑树的改动?
5.JDK 1.8 对 HashMap 除了红黑树还进行了哪些改动?
6.Java 中有哪些集合类?请简单介绍。
7.为什么 Java 中 HashMap 的默认负载因子是 0.75?
8.Java 中 HashMap 的扩容机制是怎样的?
9.为什么 HashMap 在 Java 中扩容时采用 2 的 n 次方倍?
10.数组和链表在 Java 中的区别是什么?
1.Java 中有哪些集合类?请简单介绍。(中)
(一般用于暖场,简单讲讲即可)
Java 集合框架(Java Collections Framework)是 Java 核心库中用于存储和操作数据集合的标准架构,主要分为 Collection(单元素集合)和 Map(键值对集合)两大接口体系。
Collection:
Map:
图片来源
1)Collection 接口(单元素集合)
List 接口(有序、可重复)
ArrayList | 动态数组 | ❌ | 随机访问快,增删中间元素慢 |
LinkedList | 双向链表 | ❌ | 增删快,随机访问慢;实现了 Deque 接口 |
Vector | 动态数组 | ✅ | 线程安全但性能差(已过时) |
Stack | 栈(继承Vector) | ✅ | 后进先出(LIFO) |
CopyOnWriteArrayList | 动态数组 + 写时复制 | ✅ | 读操作无锁,写操作复制整个数组(适合读多写少) |
Set 接口(无序、唯一)
HashSet | 哈希表 | ❌ | 基于 HashMap 实现,查询最快 |
LinkedHashSet | 哈希表 + 双向链表 | ❌ | 保持插入顺序 |
TreeSet | 红黑树 | ❌ | 元素自动排序(需实现 Comparable) |
CopyOnWriteArraySet | 数组 + 写时复制 | ✅ | 基于 CopyOnWriteArrayList |
ConcurrentSkipListSet | 跳表 | ✅ | 有序且线程安全(高并发场景) |
Queue 接口(队列)
LinkedList | 双向链表 | ❌ | 作为队列使用,支持FIFO操作 |
PriorityQueue | 堆(小顶堆) | ❌ | 元素按优先级排序(自然序或自定义比较器) |
ArrayDeque | 循环数组 | ❌ | 双端队列,性能优于 LinkedList |
ConcurrentLinkedQueue | 链表 | ✅ | 无锁并发队列(CAS 实现) |
2)Map 接口(键值对集合)
HashMap | 数组 + 链表/红黑树 | ❌ | 最常用,允许 null 键/值 |
LinkedHashMap | 哈希表 + 双向链表 | ❌ | 保持插入顺序或访问顺序(LRU 缓存基础) |
TreeMap | 红黑树 | ❌ | 键自动排序(基于红黑树) |
Hashtable | 哈希表 | ✅ | 过时,被 ConcurrentHashMap 取代 |
ConcurrentHashMap | 数组 + 链表/红黑树 + CAS | ✅ | 高并发推荐,分段锁/桶锁优化 |
3)如何选择
需要键值对 → HashMap(非并发)、ConcurrentHashMap(并发)
需要有序 → LinkedHashMap(插入序)、TreeMap(排序)
去重存储 → HashSet
队列/栈 → ArrayDeque(双端队列)、LinkedList(栈)
高并发场景 → ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue
排序需求 → TreeSet、TreeMap、PriorityQueue
2.Java 中的 List 接口有哪些实现类?(易)
Java 中的 List 接口是 Collection(单元素容器) 的子接口,表示有序、可重复的集合。常见的实现类包括:ArrayList, LinkedList, Vector, Stack, CopyOnWriteArrayList几个实现类。
ArrayList | 动态数组 | ❌ | 随机访问多,增删少 |
LinkedList | 双向链表 | ❌ | 频繁增删,少量随机访问 |
Vector | 动态数组 | ✔️(synchronized) | 遗留代码,不推荐新项目 |
Stack | 继承 Vector | ✔️ | 栈结构(推荐 Deque 替代) |
CopyOnWriteArrayList | 动态数组(COW) | ✔️ | 高并发读,低并发写 |
3.说说 Java 中 HashMap 的原理?(中)
(参考AI)
核心原理:
基于哈希表的键值对存储:
-
使用数组(称为桶或bucket)作为主干来存储数据。
-
每个数组元素通常是一个链表的头节点(Java 8后可能变为红黑树)。
put操作(存储键值对):
-
计算哈希值: 调用键(Key)对象的hashCode()方法计算其哈希值。
-
计算桶下标: 对哈希值进行特定的扰动计算(Java 8使用(h = key.hashCode()) ^ (h >>> 16)来减少碰撞),然后通过(数组长度 – 1) & hash(等价于hash % 数组长度,但效率更高)确定键值对应该存储在哪个桶(数组索引)。
-
处理碰撞(哈希冲突):
-
如果目标桶为空:直接创建一个新节点(包含Key, Value, hash)放入该桶。
-
如果目标桶不为空(发生碰撞):
-
链表: 遍历桶中的链表(或树),用equals()方法比较新Key和链表中每个节点的Key:
-
如果找到相等的Key:用新Value覆盖旧Value。
-
如果没找到相等的Key:将新节点添加到链表末尾(Java 7是头插法,Java 8改为尾插法)。
-
-
树化: 当链表长度超过阈值(默认=8)且数组总长度达到一定大小(默认>=64)时,该链表会转换为红黑树(TreeNode),以提高长链表下的查询效率(O(n) -> O(log n))。
-
-
-
扩容: 如果添加元素后,整个HashMap中元素的数量(size)超过了数组长度 * 负载因子(默认负载因子loadFactor=0.75),则触发扩容(resize):
-
创建一个新的、更大的数组(通常是原长度的2倍)。
-
重新哈希: 遍历所有旧的桶和链表/树,根据新的数组长度重新计算每个节点的桶下标,并将节点迁移到新数组中。
-
树退化: 在迁移过程中,如果树中元素数量减少到阈值以下(默认<=6),红黑树会退化为链表。
-
get操作(根据键取值):
-
计算哈希值 & 桶下标: 与put操作相同的方式计算Key的哈希值和桶下标。
-
遍历链表/树:
-
如果目标桶为空:返回null。
-
如果目标桶不为空:
-
如果桶中第一个节点(链表头或树根)的Key匹配(equals):直接返回其Value。
-
否则,遍历该桶上的链表或红黑树,用equals()方法比较查找的Key和节点的Key:
-
找到匹配的Key:返回对应Value。
-
遍历完未找到:返回null。
-
-
-
关键特性:
-
无序: 迭代顺序不保证与插入顺序一致,也不保证顺序不变。
-
允许null键和null值。
-
非线程安全: 多线程环境下并发修改可能导致死循环(Java 7头插法导致)、数据错乱或ConcurrentModificationException。需要外部同步(如Collections.synchronizedMap)或使用ConcurrentHashMap。
-
性能: 在理想情况下(无碰撞或碰撞少),get和put操作的时间复杂度接近O(1)。最坏情况(所有键都碰撞到同一个桶,退化为链表)是O(n),树化后提升为O(log n)。
4.Java 中 ConcurrentHashMap 1.7 和 1.8 之间有哪些区别?(中)
Java 7 的 ConcurrentHashMap 采用分段锁(Segment)实现,默认16个段,每个段独立加锁,允许并发写入不同段,但扩容和哈希冲突仍受段限制。Java 8 则抛弃分段锁,改用 CAS + synchronized 对单个桶(Node)加锁,并引入红黑树优化哈希冲突,扩容时支持多线程协同迁移,并发度更高且内存开销更小。此外,Java 8 新增函数式 API(如 forEach、compute),并优化了统计方法(如 size() 使用 CounterCell 分散计数竞争)。1.8 的实现更简洁高效,锁粒度更细,适应更高并发场景。
数据结构 | Segment + HashEntry (数组+链表) | Node 数组 + 链表/红黑树 |
锁粒度 | Segment 级别(粗粒度) | 桶级别(细粒度) |
锁实现 | ReentrantLock | CAS + synchronized |
哈希冲突处理 | 链表(O(n)) | 链表转红黑树(O(log n)) |
扩容 | 各 Segment 独立扩容 | 多线程协同迁移数据 |
适用场景 | 写少读多 | 高并发写入、大数据量 |
5.为什么 JDK 1.8 对 HashMap 进行了红黑树的改动? (中)
JDK 1.8 在 HashMap 中引入红黑树,主要是为了解决哈希冲突严重时长链表导致的查询性能退化(O(n))问题。当单个桶的链表长度>=8且数组长度>=64时,链表会转换为红黑树,将最坏情况下的操作时间复杂度从 O(n) 优化到 O(log n),显著提升了高冲突场景下的性能和容器的抗攻击(防哈希碰撞DoS)能力。当链表长度<=6时,红黑树会重新退化为链表,保证低冲突时链表的效率优势。
链表 | O(n) | O(1) | 冲突较少 |
红黑树 | O(log n) | O(log n) | 冲突严重 |
-
为什么阈值是8?
-
根据泊松分布,哈希冲突达到8的概率小于千万分之一
-
树节点占用空间是普通节点的两倍,平衡性能与开销
-
-
为什么需要最小树化容量64?
-
避免早期小规模哈希表的不必要树化
-
优先通过数组扩容分散节点
-
6.JDK 1.8 对 HashMap 除了红黑树还进行了哪些改动? (中)
1)哈希函数优化
-
扰动算法升级:计算索引时,新增一步 hash = key.hashCode() ^ (key.hashCode() >>> 16),将高16位与低16位异或混合,显著减少哈希冲突,使元素分布更均匀2410。
2)链表插入方式改变
-
头插法 → 尾插法:JDK 1.7 使用头插法(易导致多线程扩容死循环),1.8 改为尾插法,避免链表倒置,提升并发安全性(尽管仍非线程安全)710。
3)扩容机制重构
-
位置重计算优化:扩容时不再全量重新哈希,而是通过 (e.hash & oldCap) == 0 判断元素位置:
-
若为 0,索引不变;
-
若为 1,新索引 = 原索引 + 旧容量479。
-
-
效率提升:避免了重新计算哈希,仅需一次位操作,扩容性能大幅提高。
4)树化条件精细化
-
链表转红黑树需同时满足:
-
链表长度 ≥ TREEIFY_THRESHOLD(默认8);
-
桶数组容量 ≥ MIN_TREEIFY_CAPACITY(默认64)。
-
-
否则优先扩容而非树化,避免小表不必要的树结构开销569。
5)并发性能增强
-
虽仍非线程安全,但内部实现采用 CAS 思想(如 size 统计通过 CounterCell 分散竞争),减少锁冲突12。
数据结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
哈希计算 | 直接取模 | 高位扰动后取模 |
插入方式 | 头插法 | 尾插法 |
扩容开销 | 全量重哈希 | 位运算判断新位置 |
树化逻辑 | 无 | 长度 ≥8 且容量 ≥64 才树化 |
7.为什么 Java 中 HashMap 的默认负载因子是 0.75?(中)
Java 中 HashMap 的默认负载因子(Load Factor)设为 0.75 是经过严谨权衡的结果,主要目的是在 时间效率(查询性能) 和 空间效率(内存利用率) 之间取得最佳平衡。既保障操作效率,又避免内存浪费,适合绝大多数场景。
0.5 | ✅ 极高(冲突少) | ❌ 低(浪费 50%) | 内存充足、实时系统 |
0.75 | ✅平衡(接近 O(1)) | ✅ 较高(闲置 25%) | 默认场景 |
1.0 | ❌ 差(冲突频繁) | ✅ 高(无浪费) | 内存紧张、低频访问 |
8.Java 中 HashMap 的扩容机制是怎样的?(中)
HashMap 在元素数超过 容量×0.75 时触发扩容,新容量为当前容量的两倍。JDK 1.8 后通过 高位比特判断节点新位置(免重算哈希),用 尾插法拆分链表/树 迁移数据,避免死循环并提升效率。
具体步骤:
创建新数组: 新容量 = 旧容量的 2 倍(如 16 → 32),保证容量始终为 2 的幂(便于位运算优化)。
迁移数据: 遍历旧数组的每个桶(Bucket),重新分配每个节点到新数组:
-
JDK 1.8 优化关键:通过 e.hash & oldCap(原数组长度的二进制数) 高位比特判断位置(无需重算哈希,如16为10000,看 e.hash 第五位是否为1):
-
结果为 0 → 节点留在原索引位置(index 不变)。
-
结果为 1 → 节点迁移到新索引 = 原索引 + 旧容量(index + oldCap)。
-
-
链表拆分:若桶中是链表,按高位结果拆分为两个链表(原位置链表 + 新位置链表),保持顺序(尾插法)。
-
红黑树拆分:若桶中是红黑树,按相同逻辑拆分,若拆分后节点数 ≤6,则退化为链表。
更新引用: 将新数组设置为 HashMap 的底层存储,旧数组被 GC 回收。
重新计算阈值: 新阈值 = 新容量 × 负载因子(如 32 × 0.75 = 24)。
哈希重计算 | 所有节点重新计算 hash | 免重算(用 e.hash & oldCap 判断) |
链表迁移 | 头插法(可能死循环) | 尾插法(避免闭环) |
迁移效率 | 单节点遍历迁移 | 按高位结果批量迁移链表/树 |
9.为什么 HashMap 在 Java 中扩容时采用 2 的 n 次方倍?(中)
HashMap 采用 2 的 n 次方容量,核心是通过 (n-1) & hash 位运算替代取模,极大提升计算桶下标的效率;同时支持扩容时免重算哈希(仅需高位比特判断新位置 n & hash ),降低迁移开销,并提升哈希分布的均匀性,是性能与设计优雅性的双重优化。
具体分析:
高效计算桶下标(核心优化)
-
定位桶下标公式:index = (n – 1) & hash(n 为数组长度)。
-
当 n 为 2 的幂时:n – 1 的二进制全为 1(例如 16-1=15 → 1111)。
-
位运算替代取模:(n-1) & hash 等价于 hash % n,但位运算比取模快 10 倍以上(CPU 指令级优化)。
扩容时免重算哈希(JDK 1.8 优化)
-
扩容后新下标 = 原位置 或 原位置 + 旧容量(index 或 index + oldCap)。
-
判断逻辑:直接通过 e.hash & oldCap 的高位比特(0 或 1)决定位置,无需重新计算 hash 值。
-
例如:旧容量 16(二进制 10000),若 e.hash & 16 = 0 则位置不变,否则新位置 = 原位置 + 16。
减少哈希冲突,分布更均匀
-
如果 length 不是 2 的幂次方,(length – 1) 的二进制会有 0 位(如 length = 15 → 1110),导致某些 index 永远无法被计算到(如0001),增加哈希冲突概率。
-
2 的幂次方长度能更均匀分布元素((n-1) & hash),确保哈希值的所有有效位都参与计算,提高查询效率。
计算桶位置速度 | 1 CPU 周期 | 10+ CPU 周期 |
哈希分布均匀性 | 最优(全低位参与) | 部分桶位不可用 |
扩容元素迁移 | 位判断(O(1)) | 全量重哈希(O(n)) |
内存利用率 | 100% 有效 | 最高 87.5% |
10.数组和链表在 Java 中的区别是什么?(中)
数组基于连续的内存块,且大小固定,支持 O(1) 随机访问但增删成本高;链表基于节点,通过指针动态链接,增删 O(1) 但访问需 O(n)。在 Java 中,ArrayList 基于数组适合读多写少,LinkedList 基于链表适合频繁增删场景。
内存结构 | 连续内存块 | 非连续内存(节点分散存储) |
访问效率 | O(1)(通过下标直接寻址) | O(n)(需从头遍历) |
增删效率 | O(n)(需移动后续元素) | O(1)(仅修改指针,无需移动) |
内存占用 | 固定大小(初始化后不可变) | 动态扩容(按需增删节点) |
内存开销 | 仅存储数据 | 额外存储指针(next/prev) |
适用场景 | 频繁随机访问、数据量固定 | 频繁增删、数据量动态变化 |
本文到此结束,如果对你有帮助,可以点个赞~
后续会在合集里持续更新 Java 相关的面试题,欢迎关注~
祝各位都能拿到满意的offer~
评论前必须登录!
注册