二进制基础回顾

以下操作相对正整数的二进制而言,对非整数不太适用。

二进制转十进制



  在二进制中,位权是2的幂,所以每一位所代表的权值从右到左分别为2^(1-1) 、2^(2-1) 、... 、 2^(n-1) ,第n位的权值为2的(n-1)次幂。

所以: 100101 = 2^5 + 2^2 + 2^0 = 37。

二进制位移操作



  当一个二进制数左移一位,右补"0"的时候,这个数每一位的权值就变成了原来的两倍,那么整个数值也扩大了2倍;当这个数左移n位的时候,这个数就扩大到原来的2^n 倍。同样的,往右移动n位,左补"0",相当于除以2^n ,如果考虑到右边数位被舍弃的问题,这就相当于除以2^n 然后取整数。这和十进制是一样的,十进制数字左移n位,就是扩大到原来的10^n 倍……

“按位与”与取模

在HashMap和ThreadLocal源码中可以看到类似这样的操作:





  在这里,“按位与”操作的作用是取模,其中"n"和"len"都是数组的长度,此处代码是要把元素的hash值映射成数组的索引(下标),以此来决定该元素的存储位置。由于hash值相对于数组的长度来说很大,所以不能把hash值直接一一映射为数组下标,而是对其取模,通过余数来映射。关于详细的取模的意义,详见百度-散列表。

原理

当n等于2的次幂时,"m%n"和"m&(n-1)"等价,求证如下:

设n=16,hash=2740216402

  • 当n取2的幂时,n的二进制表示有个特点——除去左边补全的0外,数字以"1"开头,后面全是"0";n-1的二进制表示也有一个特点——n-1的二进制位数比n少一位,数位左边全是"0",右边全是"1"。

  • n-1与hash值进行“按位与”操作时,就相当于把hash前面部分舍去,只保留后面部分(这与掩码类似,实际上在源码注释部分,也把这操作称为"mask")。这实际上就是取模操作,后面的保留部分棕红色的0010就是“余数”。为什么这部分是余数?接着往下求证:

  把hash值2740216402的二进制表示拆成两部分,可变为:



解析: r为保留部分,hash=p+r

  p与n是存在倍数关系的,如下所示:



  总结上述数量关系,可得:"hash = p + r = q * n + r",又由于r比n小,所以r自然就是"hash % n"的余数,因此当n等于2的幂,hash&(n-1)=hash%n。

HashMap中的异或操作

在HashMap源码中有这样一段代码:

/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

先简单了解下java的位移操作:

  • <<:左移运算符,num << 1,相当于num乘以2
  • >>:右移运算符,num >> 1,相当于num除以2
  • >>>:无符号右移,忽略符号位,空位都以0补齐

关于异或的知识:

  • A ^ 0 = A,即当0与一个数(0/1)进行异或操作时,结果等于这个数本身,如0 ^ 0 = 0、 0 ^ 1 = 1;
  • A ^ 1 = ! A,即当1与一个数(0/1)进行异或操作,结果等于非-此数,或者说取反,如1 ^ 0 = 1, 1 ^ 1 = 0;

再来画下图看这里代码干了些什么:



  好了,图已经画出来了,接下来对该操作进行解读——代码先是这样……然后那样……最后又……

  解读个鬼啊,其实这样很难看出这段操作的意义,所以还是看代码上面的注释吧,注释写得很清楚了,这段代码主要作用是利用hashcode的结果来生成更加“散列”的哈希值hash,什么意思?接着往下看:

  接着上一小节的“按位与”讲起,思考下,如果用原始的"hashcode"执行上小节的“按位与操作”会与怎样的问题。



  引用上面的旧图分析,如果直接用原始的hashcode来取模,然后映射为数组的下标,这样会产生一个很大的问题。通常数组的长度不会太大,即上图红棕色的部分不会很长,那么原始的hashcode的“高位”对最后的余数的影响会很小,意思就是,只要hashcode后面的四位数为"0010",不管前面蓝紫色部分是什么,“hashcode&(n-1)”的结果始终为"0010",映射为数组的下标就是“2”,这样会非常容易造成“哈希冲突”(又名“哈希碰撞”)。

  所以需要采取一种策略,使得hashcode的每一位,都尽量参与运算,尽量对取模结果产生影响,充分利用hashcode的每一位,使得取模的结果更加“零散”。因此,HashMap的源码给出了以上的方法。

  hashcode长度为32位,右移16位,就是给原始的hashcode“折成两半”,把高位的一半与低位的一半对齐,然后通过异或操作把高位和低位“结合”起来。



  生成的新的hash值,其高位部分(左边16位蓝紫色部分)保留了原hashcode的高位,低位部分(红色部分)保留了原来的高位和低位的“特征”——如果原来高位部分某一位发生改变,则影响到结果的对应位;如果原来低位某一位发生改变,也同样影响到结果相应的位。

  这里有一个问题,为什么要用异或操作?因为只能用异或操作,因为“与”和“或”不能很好的保留操作数的特征:

  • 使用“与”操作时,当一个数为“0”,则结果必然为“0”,不必考虑另一个操作数;
  • 使用“或”操作时,当一个操作数为“1”,则结果必然为“1”;
  • 使用“异或”操作时,需要知道两个操作数才能决定结果。

  当用上述方法生成新的hash值后,原来的hashcode的每一位都对最终的取模结果产生了影响,这时在一定程度上可以使得生成的余数更加均匀,更加“散列”,使得发生“碰撞”的几率降低。

HashMap中的位运算的更多相关文章

  1. PHP中的位运算与位移运算(其它语言通用)

    /* PHP中的位运算与位移运算 ======================= 二进制Binary:0,1 逢二进1,易于电子信号的传输 原码.反码.补码 二进制最高位是符号位:0为正数,1为负数( ...

  2. C语言中的位运算和逻辑运算

    这篇文章来自:http://blog.csdn.net/qp120291570/article/details/8708286 位运算 C语言中的位运算包括与(&),或(|),亦或(^),非( ...

  3. Google Earth Engine 中的位运算

    Google Earth Engine中的位运算 按位运算是编程中一个难点,同时也是在我们后续处理影像数据,尤其要使用影像自带的波段比如QA波段经常会用到的一个东西.通过按位运算我们可以筛选出我们想要 ...

  4. js中的位运算

    按位运算符是把操作数看作一系列单独的位,而不是一个数字值.所以在这之前,不得不提到什么是"位": 数值或字符在内存内都是被存储为0和 1的序列,每个0和1被称之为1个位,比如说10 ...

  5. C#学习笔记-----C#枚举中的位运算权限分配

    一.基础知识 什么是位运算? 用二进制来计算,1&2:这就是位运算,其实它是将0001与0010做位预算   得到的结果是 0011,也就是3  2.位预算有多少种?(我们就将几种我们权限中会 ...

  6. C++中的位运算总结

    1)位运算 位运算是指对转换成二进制的数字进行每一位上的0.1的运算,运算涉及到五种运算:与(&),或(|),异或(^),左移(<<),右移(>>). 如下表所示:   ...

  7. 关于C/C++中的位运算技巧

    本篇文章讲述在学习CSAPP位运算LAB时的一些心得. 移位运算的小技巧 C/C++对于移位运算具有不同的策略,对于无符号数,左右移位为逻辑移位,也就是直接移位:对于有符号数,采用算术移位的方式,即左 ...

  8. C语言中的位运算的技巧

    一.位运算实例 1.用一个表达式,判断一个数X是否是2的N次方(2,4,8,16.....),不可用循环语句. X:2,4,8,16转化成二进制是10,100,1000,10000.如果减1则变成01 ...

  9. C/C++中的位运算

    位运算     位运算的运算分量只能是整型或字符型数据,位运算把运算对象看作是由二进位组成的位串信息,按位完成指定的运算,得到位串信息的结果. 位运算符有:     &(按位与).|(按位或) ...

随机推荐

  1. CSDN-Java培训 - 看看这次会有多少人跟风...

    2019年5月8日,闲来无事(最近答辩还没事......),存个档. 看看这一波风口,记录互联网+教育.

  2. H3C TCP封装

  3. 1、Dapper介绍

    1.Dapper是一个轻量级的O/R框架,性能强劲,支持原生sql与模型对象混合写法,通过DapperExtension插件可以实现纯模型的操作(零Sql)语句. 2.创建VS 项目,添加相关的依赖包 ...

  4. ES6语法~解构赋值、箭头函数、class类继承及属性方法、map、set、symbol、rest、new.target、 Object.entries...

    2015年6月17日 ECMAScript 6发布正式版本 前面介绍基本语法,  后面为class用法及属性方法.set.symbol.rest等语法. 一.基本语法:  1.         定义变 ...

  5. uni-app学习记录06-Vuex简单使用

    import Vue from 'vue' // 这里引入vuex import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Stor ...

  6. 洛谷P1809 过河问题 经典贪心问题

    作者:zifeiy 标签:贪心 题目链接:https://www.luogu.org/problem/P1809 我们假设第 \(i\) 个人过河的耗时是 \(t[i]\) ,并且 \(t[i]\) ...

  7. thinkphp3.2如何自动生成后台模块并且怎么访问

    https://blog.csdn.net/whulovely/article/details/72773729 步骤一:在入口文件index.php下绑定Admin模块(这时候别再Home同级手动创 ...

  8. Server,Servlet,ServletConfig,ServletContext,Session,Request,Response

    Server流程 解析URL->找到应用->找到Servlet->实例化Servlet->调用init->调用service->返回响应->调用destroy ...

  9. Git的使用--如何将本地项目上传到Github(两种简单、方便的方法..)

    https://blog.csdn.net/u014135752/article/details/79951802 总结:其实只需要进行下面几步就能把本地项目上传到Github 1.在本地创建一个版本 ...

  10. BAT 脚本判断当前系统是 x86 还是 x64 系统

    本文告诉大家在写 BAT 脚本的时候,如何判断当前的系统是 32 位系统的还是 64 位系统 通过注册表进行判断方法 @echo OFF reg Query "HKLM\Hardware\D ...