JDK8中的HashMap相对JDK7中的HashMap做了些优化。
接下来先通过官方的英文注释探究新HashMap的散列怎么实现
先不给源码,因为直接看源码肯定会晕,那么我们先从简单的概念先讲起
(如果你不想深入理解 请不要看括号里的内容,可以简化阅读过程)
首先,有一个问题:假如我们现在有一个容量为16的数组,现在我想往里面放对象,我有15个对象。
怎么放进去呢???
其实要解决一个问题就够了:对象要放在哪个下标???
当然最简单的方法是从0下标开始一个一个挨着往后放
看,这样就把你们的对象放满整个数组了,一个位置也没有浪费~
但是有17个对象呢?
无论无何必须有两个对象在同一个槽位(槽位指的是数组中某个下标的空间)了,如果不扩充数组的大小的话
那我们采取的策略最简单的是像上面一样先塞满数组,最后一个对象随机放到一个位置,用链表的形式把他挂在数组中某个位置的对象上。
(较新版本的JDK中 如果链表太长会变成树)
但是如果现在我们有20个对象呢???50个对象呢???100个,1000个对象呢???
每个槽位需要承受的对象数量会越来越多,如果只是一味地挂对象,而不采取合适的策略确定要加上去的对象到底放在哪个位置的话,很有可能出现下面这种状况。
那么当我们查找一个对象的时候可能遇到这种情况,
这样的话,查询效率十分低下,我们希望加上去的对象在整个数组上呈均匀分布的趋势,这样就不会出现某个槽承受了很多对象但是有的槽位承受很少对象,甚至只有一个对象的情况。
下面是我们希望的结果。
因为要查询的话最多查两次就能查到我们想要的对象了。
这样我们就不得不决定,要加入的对象在数组的下标了!
怎么确定下标呢?有一种确定下标的方法,这种确定下标的方法(算法)叫做散列。很形象吧,打散,列开。
散列的过程就是通过对象的特征,确定他应该放在哪个下标的过程。
那这个特征是什么呢???
哈希码!(hashCode的翻译)
java每个对象都有一个叫"hashCode"的标签码 和他对应,当然这个hashCode不一定是唯一的。
(在HashMap的源码中调用了key.hashCode()来获得hashCode,请注意,因为实际调用到的是运行时对象所属类的方法
[比如类A继承了类Object,A重写了Object的hashCode()方法,Object ob = new A(); ,ob.hashCode();调用的实际是类A重写后的hashCode方法
所以我们可以通过重写 hashCode() 方法来返回我们想要的hashCode值]
所以不同对象的hashCode 可能是一样的,取决于类怎么重写hashCode()
)
我们的问题可以简化为,怎么把我们的hashCode映射到下标的二进制码上呢?
现在假设我们的 hashCode 是8位的 (实际上是32位的),比如下面就是一个对象a 的hashCode
假如我们的数组大小是16,那么我们要根据hashCode 确定好数组下标,下标的范围是0~15.
该怎么确定呢?我们可以用直接映射的方法
我们发现,把hashCode 的二进制码直接映射到数组下标的二进制码上,直接把高位全部置为0,好像可以喔。
而且 因为我们用低四位去映射,所以范围会保持在0~15间,所以最后映射的结果总是没有超出范围
这样的话,上图的hashCode 的数组下标就是 7( 1 + 2 + 4 = 7, 0111的十进制=7)
但是,进一步观察,我们发现,无论高位怎么样,只要低位相同,都会映射到同一个数组下标上。
高位有 2 ^ 4 = 16 种情况,这16种情况都会瞄准同一个数组下标,何况实际上我们的hashCode是32位的,这样的话就有 2 ^ (32 - 4) = 2 ^ 28 种冲突
出现了我们之前担心的场景,许多甚至所有对象组成一条链表挂在一个位置上,这样查询效率十分低下。
这种对不同对象进行散列,但是最后得到的下标相同的情况称为hash冲突,也可以称为散列冲突,其实散列就是hash翻译过来的。
好的,正片开始!
我们来看看JDK8中的HashMap是怎么解决这种冲突的。
首先我们要知道,JDK8是怎么执行散列的
JDK8使用了掩码,即是下文注释中将提到的用来masking的数值
这个掩码是根据HashMap存储对象的数组的大小决定的,图中table就是我们所说的hash表,n - 1 被作为掩码和 传进来的hash值(也就是hashCode)
进行 & 运算。
看下面一个例子更明了一点。
比如大小为 32 的hash表
32的二进制数是 0010 0000
那么32 - 1 = 31 就是 0001 1111
0001 1111 & A 会得到什么呢,0001 1111 像一块掩布一样,将和他 & 的数 A 的前三位都遮住,全部变成0,其他位不变,所以被称为掩码。
比如 A = 1101 0101
因为我们的掩码前三位全是0 那么他就会把A的前三位全部掩盖掉,掩码后面的1,和A对应位 & 之后保持不变
现在再来看看官方源码的hashCode是怎么减少冲突的。
来看hash 方法上的一段注解, hash方法是把hashCode再散列一次,把散列hashCode后的值作为返回值返回,以此再次减少冲突,而过程是把高位的特征性传到低位。
每个 [] 中的内容都是对前面一小段的解释,如果嫌麻烦可以直接读解释,不读英文
/**
* Computes key.hashCode() [计算得出hashCode 不归hash函数管] and spreads (XORs) higher bits of hash
* to lower[把高位二进制序列(比如 0110 0111 中的 0110) , 的特征性传播到低位中,通过异或运算实现]. Because the table uses power-of-two masking[HashMap存储对象的数组容量经常是2的次方,这个二的次方(比如上面是16 = 2 ^ 4) 减1后作为掩码], sets of
* hashes that vary only in bits above the current mask will
* always collide[在掩码是2^n - 1 的情况下,只用低位的话经常发生hash冲突,见上述例子]. (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)[因为有的hashCode他们的低位已经足够避免多数hash冲突了,比如我们的hashCode是八位的
并且我们的数组大小是((2 ^ 8) - 1) (0111 1111) 那么只有两种冲突情况而已,0mmm mmmm 和 1mmm mmmm 会冲突,每次进行插入元素或者查找元素都要调用hash函数再一次散列hashCode,显然不划算], 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);
}
什么意思呢?什么叫做把高位的特征也用上?
比如我们之前说的。当我们有一个大小为16的数组,下面是两个对象的hashCode
0110 0111
1100 0111
如果我们直接用这两个未经hash函数处理的hashCode 通过JDK的方法得出下标:
n = 16
16 = 0001 0000
16 - 1 = 15 = 0000 1111
hash(这是上图蓝字变量) = hashCode(未经hash函数再散列)
0110 0111 & 0000 1111 = 0000 0111 ------ 7
1100 0111 & 0000 1111 = 0000 0111 ------ 7
求得同一个下标,显然冲突了,就算两个hashCode他们的高位不同,但还是会冲突
现在我们用上高位的特性,
因为本来hashCode是32位的,所以上面 >>> 的是16,也就是高一半的位移到低一半去
而我们设置的hashCode 是8位的,所以上面的 >>> 的应该是 4
hash (上面蓝字变量) = hash (hashCode) ------ hash函数对hashCode 再散列
对应过程如下图
正如我们所见,原本冲突的低四位,把高位的特征传到他们上面后,他们不冲突了!
当我们对这些再散列后的结果用掩码掩掉不必要的高位之后(见上面的红框框图)(比如高四位),剩下的是
0000 1011
0000 0001
对应的数组下标是 11 和 1
解决了冲突!
关于HashMap的扩容篇正在路上~