最近研读了Hashmap的源码(jdk1.8)
对其中的三个方法有了新的认知,在此记录下来。
如有错误请告知!
1 数组求下标的核心方法
i=hash&(length-1)
假设我们获得的hash满足期望(均匀的随机 如1~15随机出现但不重复)
为了使数据分布均匀 减少hash碰撞 我们一般会采取hash%length运算 但是取模运算效率低 我们采用了与运算。
因为取模运算是10进制的概念运算 与运算是二进制的概念运算
计算机底层是二进制的 使用取模运算要先把数据转换成2进制 这一步消耗资源 所以与运算更加高效
但是这两个方法需要在特定的情况下才等价 :要使hash%length =hash&(length-1) 需要length为2的n次方
假定hashcode=19 数组长度=16 19%16=3 , 19&(16-1)=3
hash值: 0000 0001 0011
数组长度-1: 0000 0000 1111
结果: 0000 0000 0011 (等价于 19%16=3)
但是如果长度不是2的指数次幂的话 假定hashcode=19 数组长度=15 19%15=4 , 19&(15-1)=2
hash值: 0000 0001 0011
数组长度-1: 0000 0000 1110
结果: 0000 0000 0010 (不等价于 19%15=4)
19&15 可以理解为
19=1×16+3 把1×16转换为2进制 就是上面 0000 0001 0011 中第二部分的0001, 3就是第三部分 0011
第二部分与上0000 还是0000 第三部分与上1111 等于第三部分自己 相当于去掉了1个16 等价于10进制的取模运算
由此可得 hashmap的长度一定要是2的指数次幂 不是的话就两种运算不等价
2 使hashcode后半部分更加均匀的方法
hashcode 异或 hashcode无符号右移16位 获得一个比较均匀的hashcode
(h = key.hashCode()) ^ (h >>> 16)
由于取下标采用了hash&(length-1),进入计算的有效位数取决于length(数组长度)的大小,一般我们的数组长度都不会太大,这样造成了进行计算的仅仅是hashcode的后几位,hash碰撞理所当然会增多,为了减小碰撞
我们可以让hashcode后半部分更加均匀一点.
此算法可以理解为让 二进制hash前16位与后16位异或计算 所有元素全部参与计算后16位的值 保证了相对均匀性
与运算容易偏向0 (75%概率0) ,或运算容易偏向1(75%概率1) 所以选取异或运算 (50%概率1 ,50%概率0)
计算后的二进制hashcode后16位 是一种均匀性比较高的随机数。取下标的时候可以有效降低hash碰撞
3 传入任何一个数都会返回大于等于这个数的2的n次方的数
static final int tableSizeFor(int cap) {
//n=参数-1
int n = cap - 1;
n |= n >>> 1; //n自或上自己无符号右移1位
n |= n >>> 2; //n自或上自己无符号右移2位
n |= n >>> 4; //n自或上自己无符号右移4位
n |= n >>> 8; //n自或上自己无符号右移8位
n |= n >>> 16;//n自或上自己无符号右移16位
//返回n+1
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
前面说了 hashmap的长度一定要是2的指数次幂,但是hashmap的构造方法支持传入任意长度
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity); //此方法对长度进行计算
}
其实看下源码我们就可以发现 最终还是调用了tableSizeFor()方法对数组长度进行了一个计算
传入任何一个数都会返回大于等于这个数的2的n次方的数
int n = cap - 1;
n |= n >>> 1; //n自或上自己无符号右移1位
n |= n >>> 2; //n自或上自己无符号右移2位
n |= n >>> 4; //n自或上自己无符号右移4位
n |= n >>> 8; //n自或上自己无符号右移8位
n |= n >>> 16;//n自或上自己无符号右移16位
return n+1;
一个非0的int数用2进制表示,可以表示为32位的二进制数,
如 19: 0000 0000 0000 0000 0000 0000 0001 0011
最高有效位总是为1,
n |= n >>> 1 可以让最高有效位前2位为1
n: 0000 0000 0000 0000 0000 0000 0001 0011
n >>> 1: 0000 0000 0000 0000 0000 0000 0000 1001
结果: 0000 0000 0000 0000 0000 0000 0001 1011
n |= n >>> 2 可以让最高有效位前4位为1
n: 0000 0000 0000 0000 0000 0000 0001 1011
n >>> 1: 0000 0000 0000 0000 0000 0000 0000 1101
结果: 0000 0000 0000 0000 0000 0000 0001 1111
n |= n >>> 4 可以让最高有效位前8位为1
......
n |= n >>> 8 可以让最高有效位前16位为1
......
n |= n >>> 16 可以让最高有效位前32位为1 经过五次自或之后,任意n都变成了2的n次方减一
......
return n+1; //n为2的n次方减一,n+1代表2的n次方
为什么n要先减一? int n = cap - 1;
因为如果方法参数为16 如果不减1 ,返回的将会是32,所以他让16先减一 为15 这样方法的返回值就是16。相对于返回数组长度为32,这样做可以不浪费内存空间。
如有错误请告知!
如有错误请告知!!
如有错误请告知!!!