当前位置: 首页 > article >正文

一文讲解Java中HashMap的扩容机制

HashMap扩容时,会创建一个新的数组,其容量是原数组容量的两倍。然后将键值对放到新计算出的索引位置上。一部分索引不变,另一部分索引为“原索引+旧容量”

为了便于理解,结合JDK7和JDK8两个版本来讲。

在 JDK 7 中,定位元素位置的代码是这样的:

static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}

其实就相当于用键的哈希值和数组大小取模,也就是 hashCode % table.length

那我们来假设:

  • 数组 table 的长度为 2
  • 键的哈希值为 3、7、5

取模运算后,键发生了哈希冲突,都到 table[1] 上了。那么扩容前就是这个样子。
在这里插入图片描述
数组的容量为 2,key 为 3、7、5 的元素在 table[1] 上,需要通过拉链法来解决哈希冲突。

假设负载因子 loadFactor 为 1,也就是当元素的个数大于 table 的长度时进行扩容。
扩容后的数组容量为 4。

  • key 3 取模(3%4)后是 3,放在 table[3] 上。
  • key 7 取模(7%4)后是 3,放在 table[3] 上的链表头部。
  • key 5 取模(5%4)后是 1,放在 table[1] 上。
    在这里插入图片描述
    7 跑到 3 的前面了,因为 JDK 7 使用的是头插法。
e.next = newTable[i];

同时,扩容后的 5 跑到了下标为 1 的位置。

最好的情况就是,扩容后的 7 在 3 的后面,5 在 7 的后面,保持原来的顺序。

JDK 8 完全扭转了这个局面,因为 JDK 8 的哈希算法进行了优化,当数组长度为 2 的幂次方时,能够很巧妙地解决 JDK 7 中遇到的问题。

JDK 8 的扩容代码如下所示:

Node<K,V>[] newTab = new Node[newCapacity];
for (int j = 0; j < oldTab.length; j++) {
    Node<K,V> e = oldTab[j];
    if (e != null) {
        int hash = e.hash;
        int newIndex = hash & (newCapacity - 1); // 计算在新数组中的位置
        // 将节点移动到新数组的对应位置
        newTab[newIndex] = e;
    }
}

新索引的计算方式是 hash & (newCapacity - 1),和 JDK 7 的 h & (length-1)没什么大的差别,差别主要在 hash 方法上,JDK 8 是这样:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

将键的hashCode()返回的 32 位哈希值与这个哈希值无符号右移 16 位的结果进行异或。

JDK 7 是这样:

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
    //将原始哈希码h右移 20 位和 12 位,然后与h进行异或操作,接着再将结果右移 7 位和 4 位,再次与结果进行异或操作。通过这样的移位和异或操作,对原始哈希码进行了重新组合,使得最终得到的哈希值在哈希表中能够更均匀地分布,从而减少哈希冲突的概率,提高哈希表的性能。
}

我们用 JDK 8 的哈希算法来计算一下哈希值,就会发现别有洞天。
假设扩容前的数组长度为 16(n-1 也就是二进制的 0000 1111,1X2^0+ 1X2^1+ 1X2^2+ 1X2^3 =1+2+4+8=15),key1 为 5(二进制为 0000 0101),key2 为 21(二进制为 0001 0101)。

  • key1 和 n-1 做 & 运算后为 0000 0101,也就是 5;
  • key2 和 n-1 做 & 运算后为 0000 0101,也就是 5。
  • 此时哈希冲突了,用拉链法来解决哈希冲突。

现在,HashMap 进行了扩容,容量为原来的 2 倍,也就是 32(n-1 也就是二进制的 0001 1111,1X2^0+ 1X2^1+ 1X2^2+ 1X2^3+ 1X2^4=1+2+4+8+16=31)。

  • key1 和 n-1 做 & 运算后为 0000 0101,也就是 5;
  • key2 和 n-1 做 & 运算后为 0001 0101,也就是 21=5+16,也就是数组扩容前的位置+原数组的长度。

神奇吧?
在这里插入图片描述

换句话说,在 JDK 8 的新 hash 算法下,数组扩容后的索引位置,要么就是原来的索引位置,要么就是“原索引+原来的容量”,遵循一定的规律。

当然了,这个功劳既属于新的哈希算法,也离不开 n 为 2 的整数次幂这个前提,这是它俩通力合作后的结果 hash & (newCapacity - 1)
在这里插入图片描述

那你说说扩容的时候每个节点都要进行位运算吗,如果我这个HashMap里面有几十万条数据,都要进行位运算吗?

在 JDK 8 的新 hash 算法下,数组扩容后的索引位置,要么就是原来的索引位置,要么就是“原索引+原来的容量”,遵循一定的规律。

具体来说,就是判断原哈希值的高位中新增的那一位是否为 1,如果是,该元素会被移动到原位置加上旧容量的位置;如果不是,则保持在原位置。

  • 假设原数组长度为 oldCap,扩容后数组长度为 newCap = oldCap * 2。在二进制表示中,newCapoldCap 多了一位高位。例如,原数组长度 oldCap = 16(二进制为 0001 0000),扩容后 newCap = 32(二进制为 0010 0000)。

    当进行元素迁移时,对于原数组中的每个元素,我们只需要判断其哈希值与原数组长度进行按位与运算的结果:

    • 如果 (e.hash & oldCap) == 0,说明原哈希值的高位中新增的那一位为 0,元素在新数组中的位置与原数组相同。
    • 如果 (e.hash & oldCap) != 0,说明原哈希值的高位中新增的那一位为 1,元素在新数组中的位置为原位置加上旧容量。

所以,尽管有几十万条数据,每个数据项的位置决定仅需要一次简单的位运算。位运算的计算速度非常快,因此,尽管扩容操作涉及遍历整个哈希表并对每个节点进行操作,但这部分操作的计算成本是相对较低的。


http://www.kler.cn/a/529353.html

相关文章:

  • Python闭包:解锁函数式编程的隐藏力量
  • MySQL 如何深度分页问题
  • C++ 中的类(class)和对象(object)
  • 物联网 STM32【源代码形式-使用以太网】连接OneNet IOT从云产品开发到底层MQTT实现,APP控制 【保姆级零基础搭建】
  • 在Ubuntu子系统中基于Nginx部署Typecho
  • 知识库管理如何推动企业数字化转型与创新发展的深层次探索
  • 解锁计算机视觉算法:从理论到代码实战
  • 小白零基础--CPP多线程
  • w186格障碍诊断系统spring boot设计与实现
  • 星际智慧农业系统(SAS),智慧农业的未来篇章
  • MP4分析工具
  • 97,【5】buuctf web [极客大挑战 2020]Greatphp
  • python算法和数据结构刷题[5]:动态规划
  • 【Springboot2】热部署开启
  • 【人工智能】 在本地运行 DeepSeek 模型:Ollama 安装指南
  • deep generative model stanford lecture note2 --- autoregressive
  • Windows11 不依赖docker搭建 deepseek-R1 1.5B版本(附 Open WebUi搭建方式)
  • openmv运行时突然中断并且没断联只是跟复位了一样
  • 如何在Intellij IDEA中识别一个文件夹下的多个Maven module?
  • 【单层神经网络】基于MXNet库简化实现线性回归
  • Python sider-ai-api库 — 访问Claude、llama、ChatGPT、gemini、o1等大模型API
  • ollama和deepseek-r1-1.5b和AnythingLLM
  • Java 有很多常用的库
  • 【4. C++ 变量类型详解与创新解读】
  • UI线程用到COM只能选单线程模型
  • [CVPR 2024] AnyDoor: Zero-shot Object-level Image Customization