#HashMap put 流程
面试官问:"HashMap 的 put 方法流程是什么?"
候选人小汤答:"先计算 key 的 hash,然后找到对应的桶位置,如果是空的就直接放入,如果有冲突就加到链表后面。"
面试官点点头:"JDK 8 有什么不同?"
小汤说:"引入了红黑树,链表太长会转成红黑树。"
面试官追问:"那 JDK 8 的 put 流程完整说一遍。"
小汤支支吾吾说不全。
【面试官心理】 这道题看似简单,但能完整说出 JDK 8 put 全流程的候选人不多。需要包括:hash 计算、桶定位、链表/红黑树插入、树化阈值判断、扩容触发条件。
#一、put 方法入口 🔴
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// hash(key) 的实现
static final int hash(Object key) {
int h;
// JDK 8:key.hashCode() ^ (key.hashCode() >>> 16)
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}为什么 hash 要做异或运算?
key.hashCode() 返回 32 位整数
hash & (length-1) 取模
如果 length 很小(如 16),只用到了 hash 的低位
高位没有参与计算 → 哈希分布不均匀
>>> 16 将高位移到低位
再与原 hash 异或 → 让高位信息影响低位
→ 更均匀的哈希分布#二、putVal 源码解析 🔴
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K, V>[] tab;
Node<K, V> first;
int n, i;
tab = table;
n = tab.length;
// 步骤一:空表,第一次 put 时初始化
if (tab == null || (n = tab.length) == 0) {
n = (tab = resize()).length; // 扩容/初始化
}
// 步骤二:计算桶位置并检查
i = (n - 1) & hash; // 相当于 hash % n(n 是 2 的幂次)
first = tab[i];
if (first == null) {
// 桶为空,直接创建节点放入
tab[i] = newNode(hash, key, value, null);
} else {
// 桶不为空,检查 key 是否已存在或需要插入
Node<K, V> e;
K k;
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k)))) {
// 情况一:key 已存在(第一个节点就是),记录下来
e = first;
} else if (first instanceof TreeNode) {
// 情况二:桶中是红黑树,调用树的插入方法
e = ((TreeNode<K, V>) first).putTreeVal(this, tab, hash, key, value);
} else {
// 情况三:桶中是链表,遍历查找
for (int binCount = 0; ; ++binCount) {
if ((e = first.next) == null) {
// 找到链表末尾,插入新节点
first.next = newNode(hash, key, value, null);
// 判断是否需要树化
if (binCount >= TREEIFY_THRESHOLD - 1) { // -1 因为 binCount 从 0 开始
treeifyBin(tab, i);
}
break;
}
// 链表中找到相同的 key
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
break; // e 指向已存在的节点
}
first = first.next; // 继续遍历
}
}
// 步骤三:key 已存在,更新值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) {
e.value = value; // 更新值
}
afterNodeAccess(e); // LinkedHashMap 使用,用于维护访问顺序
return oldValue; // 返回旧值
}
}
++modCount;
// 步骤四:检查是否需要扩容
if (++size > threshold) {
resize();
}
afterNodeInsertion(evict); // LinkedHashMap 使用
return null;
}#三、流程图解 🔴
graph TD
A["put(key, value)"] --> B{"table == null ?"}
B -->|是| C["resize() 初始化 table"]
C --> D["计算桶位置<br/>i = (n-1) & hash"]
B -->|否| D
D --> E{"桶是否为空 ?"}
E -->|是| F["直接放入新节点"]
E -->|否| G{"首个节点的<br/>key 匹配 ?"}
G -->|是| H["记录 e = first"]
G -->|否| I{"首个节点是<br/>TreeNode ?"}
I -->|是| J["treePutTreeVal()<br/>红黑树插入"]
I -->|否| K["遍历链表"]
K --> L{"找到相同 key ?"}
L -->|是| M["记录 e = 该节点"]
L -->|否| N["插入链表尾部<br/>binCount++"]
N --> O{"binCount >= 7 ?"}
O -->|是| P["treeifyBin()<br/>链表转红黑树"]
O -->|否| Q["继续遍历"]
Q --> L
P --> R
M --> R
J --> R
H --> R
F --> S
S --> T{"size > threshold ?"}
T -->|是| U["resize() 扩容"]
T -->|否| V["返回"]
U --> V
R --> T#四、关键细节 🔴
#4.1 为什么用 (n - 1) & hash 而不是 hash % n?
i = (n - 1) & hash; // n 是 2 的幂次,如 16, 32, 64...
// n = 16 时,n-1 = 15 = 0b1111
// hash = 26 = 0b11010
// (n-1) & hash = 0b1111 & 0b11010 = 0b1010 = 10
// hash % 16 = 10
// 位运算比取模快 10 倍以上!
// 条件:n 必须是 2 的幂次(HashMap 扩容时保证)#4.2 treeifyBin 不会立刻树化
final void treeifyBin(Node<K, V>[] tab, int index) {
Node<K, V> e;
// 如果容量小于 MIN_TREEIFY_CAPACITY (64),只做扩容
// 不树化
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) {
resize(); // 先扩容试试,说不定冲突就少了
} else {
// 容量够大,真正树化
// ...
}
}⚠️
链表长度达到 8 不一定会立刻树化。如果当前 table 容量小于 64,会先扩容。只有容量大于等于 64 后,链表达到 8 才会真正树化。
#五、追问升级
面试官:"put 的时候,如果 key 已经存在,会发生什么?"
map.put("key", "value1"); // 第一次 put
map.put("key", "value2"); // 第二次 put
// 流程:
// 1. 找到 key 所在的节点
// 2. 覆盖 value
// 3. 返回旧值 "value1"
// 4. size 不变,modCount++面试官:"ConcurrentHashMap 的 put 和 HashMap 有什么区别?"
// 主要区别:
// 1. ConcurrentHashMap 用 CAS + synchronized,而不是单个锁
// 2. 初始化用 casTabAt() 而非直接赋值
// 3. 红黑树节点插入后用 synchronized 加锁
// 4. 扩容是支持并发的多线程扩容【面试官心理】 能说出 ConcurrentHashMap 和 HashMap put 流程差异的候选人,说明对并发集合有深入了解。这是 P6+ 的要求。