HashMap 抽背清单
一、 底层结构篇 (The Structure)
- JDK 1.7 和 JDK 1.8 的底层数据结构有什么区别?关键词:JDK1.7=数组+链表;JDK1.8=数组+链表+红黑树
- 为什么要引入红黑树?它是为了解决什么具体问题?关键词:解决哈希冲突导致的链表过长问题;查询复杂度从 O(N) 优化到 O(log N)
- 链表转红黑树的阈值是多少?是只要链表长度 > 8 就立刻转吗?关键词:链表长度 > 8 且 数组容量 >= 64 (否则优先扩容)
- 为什么选择红黑树而不是 AVL 树(平衡二叉树)?关键词:红黑树是“宽松平衡”,插入删除时旋转次数少,性能优于 AVL(严格平衡)
二、 核心机制篇 (The Mechanics)
- HashMap 的默认初始容量是多少?为什么规定容量必须是 2 的幂次方?关键词:16;利用位运算 (n-1) & hash 快速求下标;保证散列均匀
- Hash 索引是如何计算的?(数组下标 index 是怎么由 Key 算出来的?)关键词:Key.hashCode() ^ (h >>> 16) (扰动函数);然后 (n - 1) & hash
- 负载因子(Load Factor)为什么默认是 0.75?这个数字背后的逻辑是什么?关键词:泊松分布统计学结果;空间利用率与 Hash 冲突概率的最佳折中点
- 请详细描述一下
put方法的完整执行流程。关键词:算Hash -> 没冲突直接放 -> 有冲突看equals -> 覆盖或挂链表/树 -> 长度超阈值转树 -> 元素超阈值扩容
三、 扩容与性能篇 (Resizing)
- 扩容(Resize)是什么时候触发的?(具体的判定公式是什么?)关键词:Current Size > Threshold (即 Capacity * 0.75)
- 扩容的全过程是怎样的?JDK 1.8 在扩容时的数据搬运 (Rehash) 做了什么优化?关键词:新建 2 倍大小数组;1.8 不需要重新取余,通过高位 bit 判断(原位置 or 原位置+旧容量)
- 为什么在《阿里巴巴 Java 开发手册》中建议在
new HashMap时指定初始容量?关键词:避免频繁扩容带来的 Rehash 性能消耗;减少内存碎片
四、 键值设计篇 (Key & Value)
hashCode()和equals()的关系是什么?关键词:Hash 决定桶的位置;Equals 决定是不是同一个对象(是否覆盖)- 如果你重写了
equals但没重写hashCode,在使用 HashMap 时会发生什么 Bug?关键词:存进去的数据取不出来(Hash 不同去了错误的桶);内存泄漏 - 为什么推荐用
String或Integer这种不可变类作为 HashMap 的 Key?关键词:不可变保证 Hash 值稳定;内部缓存了 Hash 值不需要重复计算 - 如果我非要用一个可变对象(如 Student)做 Key,后来我修改了它的属性,会发生什么后果?关键词:对象 HashCode 变了;导致在 Map 中“失联”(找不到原来的桶)
五、 并发与进化篇 (Concurrency)
- HashMap 为什么是线程不安全的?(在 JDK 1.7 和 JDK 1.8 中分别表现为什么现象?)关键词:1.7 并发扩容导致环形链表(死循环);1.8 并发 Put 导致数据覆盖(丢失)
- ConcurrentHashMap 在 JDK 1.7 和 JDK 1.8 的实现原理有什么本质区别?关键词:1.7 使用 Segment 分段锁(粒度粗);1.8 使用 CAS + synchronized 锁节点(粒度细)
- 在 JDK 1.8 的 ConcurrentHashMap 中,什么时候使用 CAS?什么时候使用 synchronized?关键词:桶为空时用 CAS 占座;发生 Hash 冲突时用 synchronized 锁住链表头节点
查看20道真题和解析