java HashMap hash方法分析

下面分别分析下,JDK1.7 与 JDK1.8 中 hash方法的运算过程,而且左后结合JDK1.8 中 hash方法来进行详细说明。算法

 

JDK1.7 中HashMap 中hash table 定位算法: 数组

int hash = hash(key.hashCode()); app

int i = indexFor(hash, table.length);  函数


其中indexFor和hash源码以下: 3d

/** 
* Applies a supplemental hash function to a given hashCode, which 
* defends against poor quality hash functions. This is critical 
* because HashMap uses power-of-two length hash tables, that 
* otherwise encounter collisions for hashCodes that do not differ 
* in lower bits. Note: Null keys always map to hash 0, thus index 0. 
*/ 
static int hash(int h) { 
// 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); 
} 

/** 
* Returns index for hash code h. 
*/ 
static int indexFor(int h, int length) { 
	return h & (length-1); 
}

indexFor这个方法论坛中已有人分析过,这里就再也不分析。 code

如今分析一下hash算法: 

Java代码  blog

h ^= (h >>> 20) ^ (h >>> 12);  ip

return h ^ (h >>> 7) ^ (h >>> 4);  字符串


假设key.hashCode()的值为:0x7FFFFFFF,table.length为默认值16。 
上面算法执行以下: 

 

获得i=15 

其中h^(h>>>7)^(h>>>4) 结果中的位运行标识是把h>>>7 换成 h>>>8来看。 

即最后h^(h>>>8)^(h>>>4) 运算后hashCode值每位数值以下: 
8=8 
7=7^8 
6=6^7^8 
5=5^8^7^6 
4=4^7^6^5^8 
3=3^8^6^5^8^4^7 
2=2^7^5^4^7^3^8^6 
1=1^6^4^3^8^6^2^7^5 
结果中的一、二、3三位出现重复位^运算 
3=3^8^6^5^8^4^7     ->   3^6^5^4^7 
2=2^7^5^4^7^3^8^6   ->   2^5^4^3^8^6 
1=1^6^4^3^8^6^2^7^5 ->   1^4^3^8^2^7^5 

算法中是采用(h>>>7)而不是(h>>>8)的算法,应该是考虑一、二、3三位出现重复位^运算的状况。使得最低位上原hashCode的8位都参与了^运算,因此在table.length为默认值16的状况下面,hashCode任意位的变化基本都能反应到最终hash table 定位算法中,这种状况下只有原hashCode第3位高1位变化不会反应到结果中,即:0x7FFFF7FF的i=15。源码

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

JDK1.8中hash方法已经获得了简化

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

而且针对HashMap中的hash算法,你们看的一脸懵逼。这段代码叫“扰动函数”。

咱们知道,HashMap经过 (length - 1) & hash 的方式获取table数组的下标,而且table的长度必须为2 ^ n,由于这样前面的公式就至关于取模。

若是咱们对key的hash不进行扰动,“与”操做的结果就是hashcode的高位所有归0,只保留低位的值用来作数组下标访问。以初始长度16为例,16 - 1 = 15。2进制表示是00000000 00000000 00001111。和某散列值作“与”操做以下,结果就是借去了最低位的四位。

    10100101 11000100 00100101

&  00000000 00000000 00001111

---------------------------------------------

    00000000 00000000 00000101    // 高位所有归0,只保留末四位

但这个时候问题就来了,这样就算个人hashcode散列值再怎么松散,要是只取最后几位的话,碰撞也会很严重。更要命的是若是散列自己作的很差,分布上成等差数列的漏洞,刚好使最后几个低位呈现规律性重复,就无比蛋疼。

这时候扰动函数的价值就体现出来了,咱们根据图来分析JDK1.8 的hash方法如何扰动的

右移16位,正好是32bit的一半,本身的高半区和低半区做异或,就是混合原始哈希吗的高位和低位,以此来加大低位的随机性。而混合后的低位can泽勒高位的部分特征,这样高位的信息也被表象保留下来。

最后咱们来看一下Peter Lawley的一片专栏文章《An introduction to optimising a hashing strategy》里的一个实验:他随机选取了352个字符串,在他们散列值彻底没有冲突的前提下,对他们作低位掩码,取数组下标。

结果显示,当HashMap数组长度为512的时候,也就是用掩码取低九位的时候,在没有扰动函数的状况下,发生了103次碰撞,接近30%。而在使用了扰动函数后只有92次碰撞。碰撞减小了10%。看来扰动函数确实有功效的。

但明显Java8 以为扰动作一次就够了,作4次的话,多了可能边际效用也不大,所谓为了效率就改为了一次。

那为何必需要用 ^ 作扰动呢?

由于异或运算,实现均衡分配获得1或者0的几率都是1/2,而&(与)运算获得0的几率较大为75%,| (或)运算获得1的几率较大为75%。下面举例:

对于0/1的运算组合无非就是下面四种,咱们分别作异或、与、或运算

    0   |   1   |   1   |   0

^  1   |   0   |   1   |   0 

---------------------------

    1       1       0       0

 

    0   |   1   |   1   |   0

&  1   |   0   |   1   |   0 

---------------------------

    0       0       1       0

 

    0   |   1   |   1   |   0

|   1   |   0   |   1   |   0 

---------------------------

    1       1       1       0

咱们能够看到,对于不一样状况下的运算,只有异或运算表现出结果均匀,这也是为何扰动运算要用异或的缘由了

相关文章
相关标签/搜索