HashMap查漏补缺

简介: HashMap 是面试的钉子户了,网上分析的文章也有很多,相信大家对于原理已经烂俗于心了。但最近在看源码时,发现其中一些实现细节其实不太好理解,所以决定以问答的形式在这里记录一下,写的时候尽量把原因说明白,希望对大家有帮助容量和 size 分别指什么?容量并不是指 HashMap 所能存储的键值对数量,而是其内部的 table 数组的大小,而 size 是指目前已存储的键值对的数量。

HashMap 是面试的钉子户了,网上分析的文章也有很多,相信大家对于原理已经烂俗于心了。但最近在看源码时,发现其中一些实现细节其实不太好理解,所以决定以问答的形式在这里记录一下,写的时候尽量把原因说明白,希望对大家有帮助

容量和 size 分别指什么?

容量并不是指 HashMap 所能存储的键值对数量,而是其内部的 table 数组的大小,而 size 是指目前已存储的键值对的数量。table 是一个 Entry 数组。 table 的每一个节点都连着一个链表或者红黑树。

初始容量可以随意设置吗?

可以,但是 HashMap 内部会你设置的 initialCapacity 转换为大于等于它的最小的2的n次方。比如 20 转为 32,32 转为 32等。如果不设置,则为默认值16。需要注意的是,在 Java 8的源码中,并没有在构造方法直接新建数组。而是先将处理后的容量值赋给 threshold,在第一次存储键值对时再根据这个值创建数组。

为什么内部要将容量转换为 2 的n次方?

这样可以提高取余的效率。为了防止链表过长,要保证键值对在数组中尽可能均匀分布,所以在计算出 key 的 hash 值之后,需要对数组的容量进行取余,余数即为键值对在 table 中的 index。 对于计算机而言,二进制位运算的效率高于取余(%)操作。而如果容量是 2 的 n 次方的话,hash 值对其取余就等同于 hash 值和容量值减1进行按位与(&)操作:

// capacity 为 2 的 n 次方的话,下面两个操作结果相同
hash & (capacity -1)
等同于
hash % capacity

那为什么两种操作等同呢? 我们以2进制的思维想一下,如果一个数是 2 的 n 次方,那它的二进制就是从右往左 n 位都为0,n+1 位为1。比如2的3次方就是 1000。这个数的倍数也满足从右往左 n 位都为0,取余的时候抛弃倍数,就等同于将 n+1 位及其往左的所有位归0,剩下的 n 位就代表余数。换句话说,一个数对2的 n 次方取余,就是要取这个数二进制的最低 n 位。 2 的 n 次方减1的结果就是 n 位1,进行与操作后就得到了最低 n 位。

如何将一个值转换为大于等于它的最小的2的 n 次方?

我们先假设一个二进制数 cap,cap 的二进制有 a 位(不算前面高位的0),那么,大于它的最小的2的次方就是2的 a 次方。2 的 a 次方减一的结果就是 n 位1,那我们只要将 cap 的全部 2 进制位变为1,再加1就能得到结果。而为了防止 cap 本身就是2的 n 次方,我们在计算之前先将 cap 自减。

如何将二进制位都变成1呢?下面是源码:

static final int tableSizeFor(int cap) {
    int n = cap - 1; //这一步是为了避免 cap 刚好为2的 n 次方
    n |= n >>> 1; //保证前2位是1
    n |= n >>> 2; //保证前4位是1
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

下面的描述中 n 的位数都不包括前面补位的0。

|= 这个符号不常见,a |= b 就是 a = a|b 的意思。代码中先将 n 无符号右移1位,由于n 的第1位肯定是1,移位后第2位是1,| 操作后前2位就保证是1了。第二步右移了2位再进行|操作,保证了前4位是1,后面的计算类似,由于 n 最多有32位,所以一直操作到右移16为止。这样就将 n 的所有2进制位都变成了1,最后自增返回。

hash 值是如何计算的

hash 值并没有直接返回 hashcode 的返回值,而是进行了一些处理。 前面提到过,hash 值算出来后需要进行取余操作,影响取余结果的是 hash 值的低 n 位。如果低 n 位是固定的或者集中在几个值,那么取余的结果容易相同,导致 hash 碰撞的发生。由于 hashcode 函数可以被重写,重写者可能无意识地就写了一个坏的 hash 函数,导致上面这种情况发生。

为了避免这种情况,HashMap 将 hash 值先向右移位,再进行或操作,这样就使高位的值和低位的值融合成一个新的值,保证取余结果受每一个二进制位的影响。Java 7和 Java 8的原理都一样,但源码有细微出入,可能是 因为 Java 经过统计发现移位一次就够了吧。

//Java 7
static int hash(int h) {
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
//Java 8
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

扩容后元素如何进行移动

为了防止元素增多后,链表越来越长,HashMap 会在元素个数达到阈值后进行扩容,新的容量为旧容量的2倍。 容量变化后,每个元素用 hash 值取余的结果也会随之变化,需要在数组中重新排列。以前同一条链表上的元素,扩容后可能存在不同的链表上。

在 Java 7 中,重新排列实现得简单粗暴,直接用 hash 根据新容量算出下标,然后设置到新数组中,即相当于将元素重新put 了一次。但在 Java 8中,作者发现没必要重新插入,因为重新计算后,新的下标只可能有两种情况,要么是原来的值,要么是原来的值加旧容量。比如容量为16的数组扩容到32,下标为1的元素重新计算后,下标只可能为1或17。

这个怎么理解呢?重提一下之前的一句话,一个数对2的 n 次方取余,就是要取这个数二进制的最低 n 位。当容量为16时,取余是取最后4位的值,而扩容到32后,取余变成取最后5位的值。这里增加的1位如果为0,那么余数就没变,如果为1,那么余数就增加了16。如何取增加的这一位的值呢?直接和16进行与操作即可。16的二进制是10000,与操作后如果结果为0,即表示高位为0,否则为1。

根据这个原理,我们只需要将原来的链表分成两条新链放到对应的位置即可,下面是具体步骤:

  1. 遍历旧数组,如果元素的 next 为空,直接取余后放到新数组;
  2. 如果元素后面连了一个链表,那么新建两条链表,暂且成为 hi 链和 lo 链;
  3. 遍历链表,计算每个元素的 hash 值和旧容量与操作的结果,结果为0则将其放入 lo 链末端,不为0放入 hi 链末端;
  4. 将两条链的 head 放到新数组中,其中 loHead 放到原来的位置,hiHead 放到原来的下标加上旧容量处;
  5. 如果是红黑树,进行和上面类似的操作,只是将两条链表换成两棵树。

理解原理后看代码就很简单了:

if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
    Node<K,V> e;
    if ((e = oldTab[j]) != null) {
        oldTab[j] = null;
        if (e.next == null) 
            newTab[e.hash & (newCap - 1)] = e;
        else if (e instanceof TreeNode) //红黑树类似
            ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
        else { // preserve order
            //新建两条链表
            Node<K,V> loHead = null, loTail = null;
            Node<K,V> hiHead = null, hiTail = null;
            Node<K,V> next;
            do {
                next = e.next;
                if ((e.hash & oldCap) == 0) { //结果为0,表示下标没变,放入 lo 链
                    if (loTail == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                }
                else { //结果为0,表示下标要加上旧容量,放入 hi 链
                    if (hiTail == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                }
            } while ((e = next) != null);
            if (loTail != null) { //lo 链放在原来的下标处
                loTail.next = null;
                newTab[j] = loHead;
            }
            if (hiTail != null) { //hi 链放在原来的下标 加旧容量处
                hiTail.next = null;
                newTab[j + oldCap] = hiHead;
            }
        }
    }
}

最后 需要这份HashMap源码解析进阶学习视频的可以加Android进阶群免费获取;701740775。请备注csdn

相关文章
|
3月前
|
监控 安全 数据处理
基于控制障碍函数(CBF)的多无人机编队避障路径规划研究附MATLAB代码
✅作者简介:热爱科研的Matlab仿真开发者,擅长毕业设计辅导、数学建模、数据处理、建模仿真、程序设计、完整代码获取、论文复现及科研仿真。 🍎 往期回顾关注个人主页:Matlab科研工作室 👇 关注我领取海量matlab电子书和数学建模资料 🍊个人信条:格物致知,完整Matlab代码获取及仿真咨询内容私信。 🔥 内容介绍 一、多无人机编队应用需求与挑战 广泛的应用场景:多无人机编队在诸多领域展现出巨大潜力。在军事领域,可执行侦察、监视、攻击等任务,通过编队协同提高作战效能;在民用方面,诸如测绘、物流配送、大型活动安保等场景中,多无人机编队能够凭借集体优势,高效完成任务。例如,
|
负载均衡 网络虚拟化 C++
|
存储 算法 NoSQL
6 种常见分布式唯一ID生成策略及它们的优缺点对比
全局唯一的 ID 几乎是所有系统都会遇到的刚需。这个 id 在搜索, 存储数据, 加快检索速度 等等很多方面都有着重要的意义
6 种常见分布式唯一ID生成策略及它们的优缺点对比
|
2月前
|
存储 人工智能 弹性计算
阿里云优惠活动大全:2026最新手动整理,购买云服务器、域名、Tokens及云存储等产品的用户可以看看
2026阿里云优惠大全官网:https://t.aliyun.com/U/FzmsXA 涵盖38元轻量服务器、99元ECS、1元域名、7000万免费Tokens、学生300元无门槛券、企业上云补贴及160+款免费试用产品,一站式汇总活动中心、免费中心、云工开物等全入口。
247 3
|
人工智能 开发框架 小程序
【一步步开发AI运动APP】二、跨平台APP AI运动识别方案介绍
本系列博文旨在帮助开发者从【AI运动小程序】迈向性能更优的【AI运动APP】开发。通过「云智AI运动识别」uni-app版插件,提供本地原生极速识别、精准姿态检测及运动计时计数功能,支持健身系统、线上赛事、学生体测、康复锻炼等多场景应用。插件无需云端依赖,一次付费永久使用,成本低且扩展性强。同时兼容uni-app与uni-app x框架,适合不同技术背景的开发者快速上手,助力抢占AI辅助运动市场。下篇将介绍插件引入,敬请期待!
|
人工智能 自然语言处理 数据可视化
体验评测报告:阿里云百炼平台——大模型应用构建的全方位工具箱
体验评测报告:阿里云百炼平台——大模型应用构建的全方位工具箱
1416 2
|
消息中间件 存储 算法
解读 RocketMQ 5.0 全新的高可用设计
本文主要介绍高可用架构的演进以及RocketMQ 5.0 全新的高可用设计。
12800 22
|
移动开发 负载均衡
【已解决】阿里云负载均衡配置后,健康检查异常(https访问502)
阿里云负载均衡配置后,健康检查异常(https访问502)
1282 0
|
算法 编译器 调度
HLS-指令使用指南(三)
HLS-指令使用指南
3117 0
HLS-指令使用指南(三)
|
机器学习/深度学习 人工智能 算法
ResNet图像识别准确率暴降40个点!这个ObjectNet让世界最强视觉模型秒变水货
MIT和IBM的研究团队近日发布一个不同寻常的目标识别数据集ObjectNet,包含50000张特意拍摄的照片,尽可能接近真实世界。该数据集让AlexNet、ResNet、Inception等最先进的图像识别模型纷纷栽倒,性能暴降40%~45%。
762 0
ResNet图像识别准确率暴降40个点!这个ObjectNet让世界最强视觉模型秒变水货