文章目录
一、为什么HashMap总被面试官翻牌子?
在Java技术岗面试中,HashMap的出镜率堪比顶流明星!!!据统计,95%的Java工程师在面试中都会被问到HashMap相关的问题。这货简直就是面试界的"钉子户",(悄悄说)甚至有些面试官开场就直接来一句:“先说说HashMap吧”。
二、底层结构到底藏着什么玄机?
先来张灵魂手绘图(想象一下):
数组 + 链表 → 数组 + 链表/红黑树(JDK8+)
每个数组元素我们称为桶(Bucket),每个桶里装的可能是:
- 单个节点(最理想情况)
- 链表(哈希冲突时)
- 红黑树(链表长度≥8且数组长度≥64时)
三、put方法执行时的暗箱操作
你以为put就是简单的存数据?太天真了!来看源码级的执行流程:
-
计算哈希值:不是直接用hashCode()!!!
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
这个扰动函数(重要!!!)能有效减少哈希碰撞
-
定位桶位置:(n-1) & hash (n是数组长度)
-
处理碰撞:
- 无节点:直接新建
- 有节点:比较key(先比哈希值,再比equals)
-
树化检查:当链表长度≥8,且数组长度≥64时转红黑树
四、初始容量16是玄学吗?
(敲黑板)这个设计其实充满智慧:
- 2的幂次方特性:(n-1) & hash 等效于取模运算,但效率更高
- 默认负载因子0.75:空间和时间成本的折中方案
- 扩容阈值=容量×负载因子:12(当默认容量16时)
五、扩容机制里的骚操作
当size超过阈值时,会发生resize(这个操作耗时!!!)。JDK8的优化点:
newTab[e.hash & (newCap-1)] = e; // 原索引位置
newTab[e.hash & (newCap-1) + oldCap] = e; // 新索引位置
利用高位判断,节点要么在原位置,要么在"原位置+旧容量"的位置,不需要重新计算哈希!!!
六、线程安全问题有多可怕?
来看这个死亡现场:
// 线程A执行到此处
if ((p = tab[i = (n - 1) & hash]) == null)
// 线程切换!!!
tab[i] = newNode(hash, key, value, null);
此时线程B也执行相同操作,就会导致数据覆盖。更可怕的是链表可能成环(JDK7的bug),导致CPU飙升到100%!
七、为什么重写equals必须重写hashCode?
来看这个恐怖故事:
Map<Student, Integer> map = new HashMap<>();
Student s1 = new Student("张三", 20);
Student s2 = new Student("张三", 20);
map.put(s1, 100);
System.out.println(map.get(s2)); // 输出null!
这就是没同时重写hashCode的后果!因为默认的hashCode()是根据内存地址计算的。
八、加载因子能不能随便改?
官方建议:不要动我的0.75!这个值是统计学得出的黄金分割点:
- 大于0.75 → 空间节省但哈希碰撞增加
- 小于0.75 → 空间浪费但查询更快
特殊场景调整建议:
- 内存紧张 → 适当调高
- 查询频繁 → 适当调低
九、遍历时顺序不可靠?
这可不是bug是feature!HashMap的遍历顺序:
- 从数组的0号索引开始
- 逐个遍历每个桶
- 每个桶里按链表/树结构遍历
所以当发生扩容后,遍历顺序可能完全改变!(重要)
十、红黑树转换的隐藏条件
记住两个必要条件:
- 链表长度≥8
- 数组长度≥64(这个很多人会忘!!!)
否则只会触发扩容而不是树化。设计者考虑到在小容量时,扩容比树化更划算。
高频面试题集锦(背熟保命)
- 为什么用红黑树不用AVL树?
- 头插法和尾插法有什么区别?
- ConcurrentHashMap如何保证线程安全?
- 为什么建议用String/Integer作为key?
- 怎么设计一个不可变类作为key?
最后的小贴士
理解HashMap的最好方式是:
- 手动画出put过程(别偷懒!)
- 用IDEA的Debug模式单步跟踪
- 自己实现一个简化版HashMap(强烈推荐)
下次面试再被问HashMap,记得笑着对面试官说:“您想听哪个版本的实现?JDK7还是JDK8的?” (装X有风险,使用需谨慎)