HashMap的底层数据结构是数据加链表,在JDK1.8中,当链表元素超过8时,会将链表转为树。数组的长度是有限的,默认情况下为16,不考虑扩容的情况下,如果有17个元素将如何放入HashMap中呢?这就是本文要讲解的问题。
有一个问题:假如我们现在有一个容量为16的数组,现在我想往里面放16个对象,怎么放进去呢?其实只需解决一个问题就够了,对象要放在数组的哪个下标处。最简单的方式是从0下标开始,一个挨着一个放
看,这样就把咱们的16个对象放满整个数组了,一个位置也没有浪费。
如果现在有17个对象需要放进去呢?
无论如何必须至少有两个对象在同一个槽位(槽位指的是数组中某个下标的空间)了,如果不扩容数组大小的话,那我们采取的策略最简单的是像上面一样先塞满数组,最后一个对象随机放到一个位置,用链表的形式把它挂在数组中某个位置的对象上
但是如果现在我们有20个对象呢?100个,1000个呢?
每个槽位需要挂载的对象数量越来越多,如果只是一味的挂对象,而不采用一定的策略确定要加上的对象到底放在哪个位置的话,极端情况下很有可能出现下图的情况
那么当我们查找一个对象的时候可能遇到这种情况:
这样的话,查询效率十分低下,我们希望加上的对象在整个数组上呈均匀分布,这样就不会出现某个槽位承受了很多对象但是有些槽位承受很多对象,甚至只有一个对象的情况。下面是我们希望的结果:
这种情况下,要查询一个对象的话,最多只需要比较两次就能查找到我们想要的对象了。
因此,我们必须有一个算法来决定我们的对象到底应该放在数组的哪个下标处。
HashMap中采用的对key进行散列,在java里每个对象都有一个叫hashCode()的方法,继承至java.lang.Object,当然你也可以重写这个方法(两个对象的hashCode可能是一样的,取决于类怎么重写hashCode()方法)。
到此,我们的问题就简化为,怎么把我们的hashCode映射到数组下标的二进制码上呢?
现在假设我们的hashCode是8位的(实际上是32位的),比如下面是a对象的hashCode
假如我们的数组大小是16,那么我们要根据hashCode确定好数组下标,下标范围是0~15。
该怎么确定呢?我们可以用直接映射的方法
我们发现,把hashCode的二进制码直接映射到数组下标的二进制码上,直接把高位置为0,好像可以喔。
而且,因为我们用低四位去映射,所以范围会保持在0~15之间,所以最后映射结果总是没有超出范围。
这样的话,上图的hashCode数据下标确定为7(0111对应的十进制就是7)
但是,我们进一步观察会发现,无论高位如何,只要地位相同,都会映射到同一个数组下标处。
高位有2^4=16种情况,都会瞄准同一个数组下标,何况实际上我的hashCode是32位的,冲突的情况更多。出现了我们之前担心的场景,许多甚至所以对象组成的一条链表挂载同一个位置上,查询效率低。
这种对不同对象进行散列,但是最后得到的下标相同的情况称为hash冲突,也可以称为散列冲突,其实散列就是hash翻译过来的。
好的,正片开始!!!
我们来看看JDK1.8中HashMap是怎么解决冲突的?
首先我们要知道,是怎么进行散列的。JDK1.8中使用了掩码,即是下文注释中将提到的用来masking的数值
这个掩码是根据HashMap存储对象的数组长度决定的,图中的table就是我们说是的hash表,n - 1被用为掩码和传进来的hash值进行&运算。
下面来看一个具体例子:
比如大小为32的hash表,32的二进制数为0010 0000,那么掩码32 - 1 = 31就是0001 1111
掩码0001 1111 & A会得到什么呢,其回像一块抹布一样,将和它进行&的数A的前三位都遮住,全部变为0,其他位保持不变,所有称为掩码。
比如A = 1101 0101,与掩码&运算之后为:0001 0101,对应的二进制数为21。与掩码进行&运算,保证了最后得到的结果永远不会超出数组范围。
现在再来看看官方源码的hashCode是怎么减少冲突的?
hash = hash(hashCode) —– hash函数对hashCode进行来再散列,对应过程如下图:
正如我们所见,原本冲突的低四位,把高位的特征传到他们上面后,他们不冲突了!