HashMap数据结构深挖(五)之数据插入详讲

HashMap数据结构深挖(五)之数据插入详讲

1. 概述

HashMap是Java集合框架中常用的数据结构,基于哈希表实现,提供了高效的键值对存储和查询功能。本文将详细剖析HashMap的数据插入过程,包括源码分析和关键步骤解释。

2. 数据插入流程

HashMap的数据插入主要通过put(K key, V value)方法完成。以下是其核心流程:

2.1 put方法入口

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
  • 参数说明
    • key:键对象。
    • value:值对象。
  • 返回值:如果键已存在,返回旧值;否则返回null

2.2 putVal方法核心逻辑

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 1. 检查哈希表是否为空或长度为0,若是则初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2. 计算索引位置,并检查该位置是否为空
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null); // 直接插入新节点
    else {
        Node<K,V> e; K k;
        // 3. 检查首节点是否匹配
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p; // 键已存在,记录旧节点
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 红黑树插入
        else {
            // 4. 遍历链表查找匹配的节点
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null); // 插入到链表尾部
                    if (binCount >= TREEIFY_THRESHOLD - 1) // 检查是否需要树化
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break; // 键已存在,记录旧节点
                p = e;
            }
        }
        // 5. 处理键已存在的情况
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value; // 更新值
            afterNodeAccess(e);
            return oldValue;
        }
    }
      modCount++;
    // 6. 检查是否需要扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

2.3 关键步骤说明

  1. 初始化哈希表:如果哈希表为空,调用resize()方法初始化。
  2. 计算索引位置:通过(n - 1) & hash计算键值对的存储位置。
  3. 处理哈希冲突
    • 如果首节点匹配,直接更新值。
    • 如果是红黑树节点,调用putTreeVal方法插入。
    • 如果是链表,遍历查找匹配的节点或插入到链表尾部。
  4. 扩容检查:如果键值对数量超过阈值,调用resize()方法扩容。

3 判断是否需要扩容

在插入数据之前,需要检查HashMap的底层数组(table)是否为空或者长度为0。如果是,则需要进行扩容操作:

if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
  • table: HashMap的底层数组,用于存储键值对。
  • resize(): 扩容方法,会重新分配数组大小并重新计算哈希值。
  • n: 扩容后的数组长度。

4 哈希值的扰动

在HashMap中,插入数据的第一步是对键(key)的哈希值进行扰动处理。这是为了减少哈希冲突的概率。扰动处理的代码如下:

(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  • key.hashCode(): 调用键的hashCode()方法获取原始哈希值。
  • h >>> 16: 将哈希值右移16位,相当于将高16位移动到低16位。
  • ^: 异或操作,将原始哈希值的高16位和低16位进行混合,增加哈希值的随机性。
  • key == null: 如果键为null,则哈希值为0,HashMap允许键为null

5 计算下标并插入数据

根据扰动后的哈希值计算数据在数组中的下标:

tab[i = (n - 1) & hash]
  • n - 1: 数组长度减1,用于与哈希值进行按位与操作,确保下标在数组范围内。
  • &: 按位与操作,相当于取模运算,但效率更高。

如果对应下标的位置为空,则直接插入数据;否则需要处理哈希冲突。

6 处理哈希冲突

如果目标位置不为空,则需要判断该位置是链表节点还是树节点:

6.1 链表节点

如果是链表节点,则遍历链表:

// 处理哈希冲突时的三种情况:键已存在、树节点、链表遍历
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
    // 情况1:当前节点p的键与插入键完全匹配(哈希值相同且equals为true)
    // 将p赋值给临时变量e,后续将用新值覆盖旧值
    e = p;
else if (p instanceof TreeNode)
    // 情况2:当前节点是红黑树节点
    // 调用红黑树的插入方法,返回可能存在的重复节点e(若存在则覆盖值)
    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 情况3:当前是链表结构,开始遍历链表
    for (int binCount = 0; ;++binCount) {
     // 子情况3.1:到达链表尾部仍未找到相同键
            // 创建新节点并插入链表尾部
        if ((e = p.next) == null) {
            p.next = newNode(hash, key, value, null);
              // 判断链表长度是否达到树化阈值(TREEIFY_THRESHOLD=8,binCount从0计数,故>=7时触发)
            if (binCount >= TREEIFY_THRESHOLD - 1) 
            // 将链表转换为红黑树(需满足数组长度≥MIN_TREEIFY_CAPACITY=64)
                treeifyBin(tab, hash);
            break;// 结束链表遍历
        }
        if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
         // 子情况3.2:在链表中找到相同键的节点e,跳出循环后续覆盖值
            break;
            // 指针后移,继续遍历链表
        p = e;
    }
}
  • p: 当前节点。
  • e: 临时节点,用于遍历链表。
  • TREEIFY_THRESHOLD: 链表转换为红黑树的阈值,默认为8。
  • treeifyBin: 将链表转换为红黑树的方法。
  • 链表插入数据关键步骤说明

​​哈希冲突处理逻辑​​
①该段代码是 HashMap 解决哈希冲突的核心逻辑,分为三种情况处理(键重复、树节点插入、链表遍历插入)
②p.hash == hash 优先比较哈希值,避免频繁调用 equals() 提升性能
​​树化条件​​
①链表长度≥8(TREEIFY_THRESHOLD)且数组长度≥64时才会树化,否则仅扩容数组
②树化操作通过 treeifyBin() 实现,涉及红黑树重构
​​链表遍历优化​​
①binCount 统计链表长度(从0开始),循环中通过 p = e 实现指针后移
②插入新节点时采用尾插法(JDK 8 改进,以前是头插法这有可能导致多线程扩容死循环)
​​​​重复键处理​​​​
无论链表还是树结构,发现重复键时都会通过 ​​e.value = value​​覆盖旧值(由外层代码控制)

6.2 树节点

如果是树节点,则调用树节点的插入方法:

e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  • putTreeVal: 向红黑树中插入节点的方法。

7 判断是否需要扩容

在插入完成后,需要检查HashMap的元素数量是否超过阈值(threshold),如果超过则进行扩容:

if (++size > threshold)
    resize();
  • size: HashMap中键值对的数量。
  • threshold: 扩容阈值,等于容量乘以负载因子。

8 总结

HashMap的数据插入过程涉及哈希值的扰动、数组扩容、哈希冲突处理等多个步骤。通过扰动哈希值减少冲突,通过链表和红黑树处理冲突,最终保证HashMap的高效性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

潇湘Victor.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值