HashMap put() 方法底层原理及冲突处理机制

一、put() 方法核心流程

1. 基本执行流程

  1. 计算键的哈希值hash = hash(key)

  2. 确定桶位置index = (n - 1) & hash(n为数组长度)

  3. 处理桶位置元素

    • 桶为空:直接插入新节点

    • 桶不为空:处理哈希冲突

2. 哈希冲突处理详细步骤

当发生哈希冲突时(即计算的桶位置已有元素),HashMap 的处理逻辑如下:

// 当前桶的第一个节点
Node<K,V> p = tab[index];

// 情况1:检查第一个节点是否匹配
if (p.hash == hash && 
    ((k = p.key) == key || (key != null && key.equals(k)))) {
    // key完全相同,准备覆盖值
    e = p;
}
// 情况2:如果是树节点
else if (p instanceof TreeNode) {
    // 调用红黑树的插入方法
    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
}
// 情况3:链表遍历
else {
    for (int binCount = 0; ; ++binCount) {
        // 到达链表尾部
        if ((e = p.next) == null) {
            // 在尾部插入新节点(JDK8尾插法)
            p.next = newNode(hash, key, value, null);
            
            // 检查是否需要树化(链表长度≥8)
            if (binCount >= TREEIFY_THRESHOLD - 1)
                treeifyBin(tab, hash);
            break;
        }
        
        // 检查当前节点是否匹配
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) {
            break; // 找到相同key的节点
        }
        p = e; // 继续遍历下一个节点
    }
}

// 处理值覆盖
if (e != null) { // existing mapping for key
    V oldValue = e.value;
    if (!onlyIfAbsent || oldValue == null)
        e.value = value; // 覆盖旧值
    afterNodeAccess(e);
    return oldValue; // 返回旧值
}

二、链表遍历关键细节

1. 节点比较顺序

  1. 先比较哈希值:快速筛选,哈希值不同直接跳过

    • if (e.hash == hash)

  2. 再比较引用地址:== 比较,处理相同对象的情况

    • (k = e.key) == key

  3. 最后比较内容:equals() 方法比较

    • key != null && key.equals(k)

2. 插入方式演变

JDK版本插入方式特点
JDK7头插法新节点插入链表头部,多线程扩容可能导致死链
JDK8+尾插法新节点追加到链表尾部,解决死链问题

3. 树化条件检查

在链表遍历时会同时计数(binCount),当满足:

  • 链表长度 ≥ TREEIFY_THRESHOLD(默认8)

  • 桶数组长度 ≥ MIN_TREEIFY_CAPACITY(默认64)

会将链表转换为红黑树,否则优先进行扩容。

三、完整示例分析

假设有以下操作:

Map<String, Integer> map = new HashMap<>();
map.put("A", 1);  // 哈希假设计算到桶5
map.put("B", 2);  // 哈希也到桶5(冲突)

执行 map.put("C", 3) 且 "C" 也哈希到桶5:

  1. 定位到桶5,发现已有链表 A → B

  2. 开始遍历:

    • 比较A:不匹配(hash或equals)

    • 比较B:不匹配

    • 到达链表尾部(B.next为null)

  3. 执行插入:

    B.next = new Node("C", 3);  // 链表变为 A → B → C
  4. 检查长度:

    • 当前链表长度=3(<8),不触发树化

  5. 完成插入

四、关键注意事项

  1. 性能影响因素

    • 哈希函数的质量直接影响冲突概率

    • 链表过长会降低查询性能(O(n))

  2. 正确实现键对象

    class MyKey {
        private String id;
        
        @Override
        public int hashCode() {
            return id.hashCode(); // 保证相同id返回相同hash
        }
        
        @Override
        public boolean equals(Object o) {
            // 必须与hashCode()保持一致
            return o instanceof MyKey && ((MyKey)o).id.equals(this.id);
        }
    }
  3. 并发问题警示

    // 不安全的操作
    HashMap<String, Integer> unsafeMap = new HashMap<>();
    // 并发put可能导致数据丢失或死循环(JDK7)
    
    // 解决方案
    ConcurrentHashMap<String, Integer> safeMap = new ConcurrentHashMap<>();

理解 HashMap 的 put 机制对于编写高效、正确的 Java 程序至关重要,特别是在处理大规模数据或高并发场景时。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值