如何计算,一对key/value应该放在哪个哈希桶

大家都知道,hashmap底层是数组+链表(不讨论红黑树的情况),其中,这个数组,我们一般叫做哈希桶,大家如果去看jdk的源码,会发现里面有一些变量,叫做bin,这个bin,就是桶的意思,结合语境,就是哈希桶。

这里举个例子,假设一个hashmap的数组长度为4(0000 0100),那么该hashmap就有4个哈希桶,分别为bucket[0]、bucket[1]、bucket[2]、bucket[3]。

现在有两个node,hashcode分别是1(0000 0001),5(0000 0101). 我们当然知道,这两个node,都应该放入第一个桶,毕竟1 mod 4,5 mod 4的结果,都是1。

但是,在代码里,可不是用取模的方法来计算的,而是使用下面的方式:

int entryNodeIndex = (tableLength - 1) & hash;

应该说,在tableLength的值,为2的n次幂的时候,两者是等价的,但是因为位运算的效率更高,因此,代码一般都使用位运算,替代取模运算。

下面我们看看具体怎么计算:

此处,tableLength即为哈希表的长度,此处为4. 4 - 1为3,3的二进制表示为:

0000 0011

那么,和我们的1(0000 0001)相与:

0000 0001 -------- 1

0000 0011 -------- 3(tableLength - 1)

相与(同为1,则为1;否则为0)

0000 0001 -------- 1

结果为1,所以,应该放在第1个哈希桶,即数组下标为1的node。

接下来,看看5这个hashcode的节点要放在什么位置,是怎么计算:

0000 0101 -------- 5

0000 0011 -------- 3(tableLength - 1)

相与(同为1,则为1;否则为0)后结果:

0000 0001 -------- 1

扩容时,是怎么对一个hash桶进行transfer的

此处,具体的整个transfer的细节,我们本讲不会涉及太多,不过,大体的逻辑,我们可以来想一想。

以前面为例,哈希表一共4个桶,其中bucket[1]里面,存放了两个元素,假设是a、b,其hashcode分别是1,5.

现在,假设我们要扩容,一般来说,扩容的时候,都是新建一个bucket数组,其容量为旧表的一倍,这里旧表为4,那新表就是8.

那,新表建立起来了,旧表里的元素,就得搬到新表里面去,等所有元素都搬到新表了,就会把新表和旧表的指针交换。如下:

java.util.concurrent.ConcurrentHashMap#transfer

private transient volatile Node[] nextTable;

transient volatile Node[] table;

if (finishing) {

// 1

nextTable = null;

// 2

table = nextTab;

// 3

sizeCtl = (tabLength << 1) - (tabLength >>> 1);

return;

}

1处,将field:nextTable(也就是新表)设为null,扩容完了,这个field就会设为null

2处,将局部变量nextTab,赋值给table,这个局部变量nextTab里,就是当前已经扩容完毕的新表

3处,修改表的sizeCtl为:假设此处tabLength为4,tabLength << 1 左移1位,就是8;tabLength >>> 1,右移一位,就是2,。8 - 2 = 6,正好就等于 8(新表容量) * 0.75。

所以,这里的sizeCtl就是,新表容量 * 负载因子,超过这个容量,基本就会触发扩容。

ok,接着说,我们要怎么从旧表往新表搬呢? 那以前面的bucket[1]举例,遍历这个链表,计算各个node,应该放到新表的什么位置,不就完了吗?是的,理论上这么写就完事了。

但是,我们会怎么写呢?

用hashcode对新bucket数组的长度取余吗?

jdk对效率的追求那么高,肯定不会这么写的,我们看看,它怎么写的:

java.util.concurrent.ConcurrentHashMap#transfer

// 1

for (Node p = entryNode; p != null; p = p.next) {

// 2

int ph = p.hash;

K pk = p.key;

V pv = p.val;

// 3

if ((ph & tabLength) == 0){

lowEntryNode = new Node(ph, pk, pv, lowEntryNode);

}

else{

highEntryNode = new Node(ph, pk, pv, highEntryNode);

}

}

1处,即遍历旧的哈希表的某个哈希桶,假设就是遍历前面的bucket[1],里面有a/b两个元素,hashcode分别为1,5那个。

2处,获取该节点的hashcode,此处分别为1,5

3处,如果hashcode 和 旧表长度相与,结果为0,则,将该节点使用头插法,插入新表的低位;如果结果不为0,则放入高位。

ok,什么是高位,什么是低位。扩容后,新的bucket数组,长度为8,那么,前面bucket[1]中的两个元素,将分别放入bucket[1]和bucket[5].

ok,这里的bucket[1]就是低位,bucket[5]为高位。

首先,大家要知道,hashmap中,容量总是2的n次方,请牢牢记住这句话。

为什么要这么做?你想想,这样是不是扩容很方便?

以前,hashcode 为1,5的,都在bucket[1];而现在,扩容为8后,hashcode为1的,还是在newbucket[1],hashcode为5的,则在newbucket[5];这样的话,是不是有一半的元素,根本不用动?

这就是我觉得的,最大的好处;另外呢,运算也比较方便,都可以使用位运算代替,效率更高。

好的,那我们现在问题来了,下面这句的原理是什么?

if ((ph & tabLength) == 0){

lowEntryNode = new Node(ph, pk, pv, lowEntryNode);

} else{

highEntryNode = new Node(ph, pk, pv, highEntryNode);

}

为啥,hashcode & 旧哈希表的容量, 结果为0的,扩容后,就会在低位,也就是维持位置不变呢?而结果不为0的,扩容后,位置在高位呢?

背后的位运算原理(大白话)

代码里用的如下判断,满足这个条件,去低位;否则,去高位。

if ((ph & tabLength) == 0)

还是用前面的例子,假设当前元素为a,hashcode为1,和哈希桶大小4,去进行与运算。

0000 0001 ---- 1

0000 0100 ---- 旧哈希表容量4

&运算(同为1则为1,否则为0)

结果:

0000 0000 ---- 结果为0

ok,这里算出来,结果为0;什么情况下,结果会为0呢?

那我们现在开始倒推,什么样的数,和 0000 0100 相与,结果会为0?

???? ???? ----

0000 0100 ---- 旧哈希表容量

&运算(同为1则为1,否则为0)

结果:

0000 0000 ---- 结果为0

因为与运算的规则是,同为1,则为1;否则都为0。那么,我们这个例子里,旧哈希表容量为 0000 0100,假设表示为2的n次方,此处n为2,我们仅有第三位(第n+1)为1,那如果对方这一位为0,那结果中的这一位,就会为0,那么,整个数,就为0.

所以,我们的结论是:假设哈希表容量,为2的n次方,表示为二进制后,第n+1位为1;那么,只要我们节点的hashcode,在第n+1位上为0,则最终结果是0.

反之,如果我们节点的hashcode,在第n+1位为1,则最终结果不会是0.

比如,hashcode为5的时候,会是什么样子?

0000 0101 ---- 5

0000 0100 ---- 旧哈希表容量

&运算(同为1则为1,否则为0)

结果:

0000 0100 ---- 结果为4

此时,5这个hashcode,在第n+1位上为1,所以结果不为0。

至此,我们离答案好像还很远。ok,不慌,继续。

假设现在扩容了,新bucket数组,长度为8.

a元素,hashcode依然是1,a元素应该放到新bucket数组的哪个bucket里呢?

我们用前面说的这个算法来计算:

int entryNodeIndex = (tableLength - 1) & hash;

0000 0001 ---- 1

0000 0111 ---- 8 - 1 = 7

&运算(同为1则为1,否则为0)

结果:

0000 0001 ---- 结果为1

结果没错,确实应该放到新bucket[1],但怎么推论出来呢?

// 1

if ((ph & tabLength) == 0){

// 2

lowEntryNode = new Node(ph, pk, pv, lowEntryNode);

}

也就是说,假设一个数,满足1处的条件:(ph & tabLength) == 0,那怎么推论出2呢,即应该在低位呢?

ok,条件1,前面分析了,可以得出:

这个数,第n+1位为0.

接下来,看看数组长度 - 1这个数。

数组长度

2的n次方

二进制表示

1出现的位置

数组长度-1

数组长度-1的二进制

2

2的1次方

0000 0010

第2位

1

0000 0001

4

2的2次方

0000 0100

第3位

3

0000 0011

8

2的3次方

0000 1000

第4位

7

0000 0111

好了,两个数都有了,

???????0??????? -- 1 节点的hashcode,第n + 1位为0

000000010000000 -- 2 老数组

000000100000000 -- 3 新数组的长度,等于老数组长度 * 2

000000011111111 -- 4 新数组的长度 - 1

运算:1和4相与

大家注意看红字部分,还有框出来的那一列,这一列为0,导致,最终结果,肯定是比2那一行的数字小,2这行,不就是老数组的长度吗,那你比老数组小;你比这一行小,在新数组里,就只能在低位了。

反之,如果节点的hashcode,这一位为1,那么,最终结果,至少是大于等于2这一行的数字,所以,会放在高位。

参考资料

原文:https://www.cnblogs.com/grey-wolf/p/13057567.html

hashmap 扩容是元素还是数组_曹工说JDK源码(1)--ConcurrentHashMap,扩容前大家同在一个哈希桶,为啥扩容后,你去新数组的高位,我只能去低位?...相关推荐

  1. 调试JDK源码-一步一步看HashMap怎么Hash和扩容

    调试JDK源码-一步一步看HashMap怎么Hash和扩容 调试JDK源码-ConcurrentHashMap实现原理 调试JDK源码-HashSet实现原理 调试JDK源码-调试JDK源码-Hash ...

  2. java 头尾 队列_源码|jdk源码之栈、队列及ArrayDeque分析

    栈.队列.双端队列都是非常经典的数据结构.和链表.数组不同,这三种数据结构的抽象层次更高.它只描述了数据结构有哪些行为,而并不关心数据结构内部用何种思路.方式去组织. 本篇博文重点关注这三种数据结构在 ...

  3. 获取arraylist的长度_啃碎JDK源码(三):ArrayList

    前言 很久之前写过一篇有关HashMap的文章:啃碎JDK源码(四):HashMap,反响不错.本来手后面是想写篇文章来介绍ArrayList,后来事情多就忘了,今天就来好好聊聊ArrayList. ...

  4. JDK源码阅读--HashMap

    文章目录 核心部分 初始化 添加/移除元素 调试HashMap通过链表法解决hash碰撞 扩容 其他 核心部分 JDK版本: 1.8 几个重要的属性说明: size map中元素的个数,与map的容量 ...

  5. 【JDK】JDK源码分析-HashMap(1)

    概述 HashMap 是 Java 开发中最常用的容器类之一,也是面试的常客.它其实就是前文「数据结构与算法笔记(二)」中「散列表」的实现,处理散列冲突用的是"链表法",并且在 J ...

  6. JDK源码笔记-java.util.HashMap

    2019独角兽企业重金招聘Python工程师标准>>> HashMap 的存储实现 当程序试图将多个 key-value 放入 HashMap 中时,以如下代码片段为例: Java代 ...

  7. 64 源码_【ClickHouse内核】源码阅读策略

    " 摘要: 本文主要讲述如何阅读ClickHouse开源数据库代码的一些方式和技巧.主要内容如下: ClickHouse开源库简介 搭建运行环境 针对于ClickHouse库提出问题 阅读开 ...

  8. jdk源码阅读-HashMap

    前置阅读: jdk源码阅读-Map : http://www.cnblogs.com/ccode/p/4645683.html 在前置阅读的文章里,已经提到HashMap是基于Hash表实现的,所以在 ...

  9. C语言在有序的数组是否存在固定点(附完整源码)

    C语言在有序的数组是否存在固定点 C语言在有序的数组是否存在固定点的完整源码(定义,实现,main函数测试) C语言在有序的数组是否存在固定点的完整源码(定义,实现,main函数测试) #includ ...

最新文章

  1. Linux中assert头文件,linux系统下如何使用assert函数
  2. docker客户端连接远程docker服务端(export方式)
  3. 砍掉九成代码,重构并简化YOLOv5图像目标检测推理实现
  4. plsql如何显示表结构图_工地新人如何看懂图纸
  5. 博士论文致谢走红后,黄国平母校演讲再刷屏!
  6. Android打包编译shrinkResources true报错解决方案
  7. 大型Web 网站 Asp.net Session过期你怎么办
  8. 数据结构_C语言_实验一_线性结构 ——一元多项式求导
  9. 别出心裁的Linux命令学习法
  10. Tomcat8安装配置
  11. js 图片上传时加水印
  12. Excel自定义格式千分符
  13. 又一琼,又一琼......
  14. 02-linux-arm板上opencv移植--终极解决方案之buildroot基础配置(原创)
  15. 邬先生及时功成身退,是明哲保身的聪明做法 --- 我看电视剧《雍正王朝》
  16. Tracup丨先进的工作流程管理如何为你节省巨额花销?
  17. 腾讯视频下载的qlv格式转换为MP4格式
  18. 绿色手动安装MySQL数据库
  19. (ACWing yxc算法基础课笔记)差分
  20. 万物互联之~网络编程基础篇

热门文章

  1. VS2019 使用 C/C++ 动态链接库 并 进行调用
  2. 体积最小桌面linux,Tiny Core Linux - 体积最小的精简 Linux 操作系统发行版之一 (仅10多MB) - 蓝月网络...
  3. 安装服务器系统多少钱,服务器系统安装费用
  4. linux安装pl sql,Linux上安装配置InstantClient及64位系统Pl/SQL配置
  5. post和get请求的区别
  6. 追加的英文计算机,Latex同时添加中英文摘要
  7. leetcode No.141 环形链表
  8. 模拟网页行为之工具篇二
  9. 统计iOS项目代码行数
  10. html跑马灯_用Excel居然能做“跑马灯”,而且还这么简单!