标准答案(面试官最爱版)
HashMap实现原理:
- 数据结构:数组+链表/红黑树(Java 8+)
- 算法>哈希算法:
(h = key.hashCode()) ^ (h >>> 16)
- 索引计算:
(n-1) & hash
(n为数组长度) - 冲突解决:链表→红黑树(阈值=8),树→链表(阈值=6)
- 扩容机制:2倍扩容,负载因子默认0.75
用程序员黑话:
“它就是个会变形的瑞士卷——平时是夹心饼干(数组+链表),吃撑了变千层蛋糕(红黑树)”
一、初入江湖:数组的容貌焦虑
1.1 基础人设:图书馆管理员
假设你有个16层的书架(数组),每层放着:
- 空书架:等待书籍(Entry)入驻
- 单本书:直接上架(没有哈希冲突)
- 一摞书:链表结构(哈希碰撞时叠罗汉)
- 精装书柜:红黑树形态(当书堆超过8本)
// 典型书架结构
[null,Node<K,V>, // 单本书TreeNode<K,V>, // 精装书柜LinkedList<Node> // 书堆
]
1.2 哈希函数:图书编号玄学
图书上架流程:
public int hashCode() {return 作者姓氏首字母.charCodeAt(0) * 31+ 书名首字母.charCodeAt(0); // 31是玄学质数
}
现实往往是:
- 《百年孤独》和《白夜行》可能挤在同一层(哈希碰撞)
- 管理员(HashMap)碎碎念:“都怪马尔克斯和东野圭吾姓氏首字母都是B!”
二、冲突解决:书架的生存战争
2.1 链表模式:叠罗汉的艺术
当新书到来时:
- 先按哈希值找楼层
- 如果该层已有书:
- Java 7:新书放最上面(头插法)
- Java 8:新书放最下面(尾插法)
为什么改尾插法?
答:为了防止多线程扩容时形成环形链表(曾经导致CPU 100%的惨案)
2.2 红黑树模式:卷王诞生
当某层书堆超过8本:
if (binCount >= TREEIFY_THRESHOLD - 1) {treeifyBin(tab, hash); // 呼叫变形金刚!
}
变形代价:
- 需要书架总层数≥64(MIN_TREEIFY_CAPACITY)
- 消耗CPU给书堆做美甲(树化操作)
降级机制:当书减少到6本时,变回普通书堆(防止频繁转换)
三、扩容风暴:图书馆扩建计划
3.1 触发条件:书架负载率超75%
if (size > threshold) {resize(); // 警报!需要扩建!
}
扩建流程:
- 新书架扩容2倍(16→32层)
- 旧书籍重新计算楼层:
- 原楼层 或 原楼层+旧容量(二进制魔术)
- 例如旧楼层=5(0101),新楼层=5或5+16=21
3.2 死亡遍历:多线程的噩梦
未同步的HashMap在扩容时:
- 线程A正在搬书到新书架
- 线程B突然来借书
- 结果可能:拿到null、死循环、数据丢失
解决方案:
用ConcurrentHashMap——给每个书架配保安(分段锁)
四、使用禁忌:程序员的防秃指南
4.1 自定义对象的坑
class Student {String name;// 忘记重写hashCode和equals...
}// 使用后果:
map.put(new Student("张三"), 90);
map.get(new Student("张三")); // 返回null(当场秃头)
4.2 负载因子调参玄学
- 设0.9:节省内存但容易碰撞(书架挤成沙丁鱼罐头)
- 设0.5:查询快但内存翻倍(每个书架只放半本书)
黄金建议:非必要别动默认0.75,这是数学家算出来的魔法数字
五、灵魂拷问:为什么不用二叉搜索树?
- 树化成本高:链表变树要旋转染色(美甲时间)
- 退化合订本:树节点占用内存是链表的2倍
- 小数据优势:链表在少量数据时遍历更快
用生活比喻:
整理5本书用书堆(链表)更快,整理50本书才需要书柜(红黑树)