HashMap的Hash算法
HashMap的数据结构
要知道hashmap是什么,首先要搞清楚它的数据结构,在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap
也不例外。HashMap
实际上是一个数组和链表的结合体(在数据结构中,一般称之为“链表散列”),请看下图(横排表示数组,纵排表示数组元素【实际上是一个链表】)。
1
|
/**
|
1
|
static class Entry<K,V> implements Map.Entry<K,V> {
|
上面的Entry
就是数组中的元素,它持有一个指向下一个元素的引用,这就构成了链表。
当我们往HashMap
中put
元素的时候,先根据key
的hash
值得到这个元素在数组中的位置(即下标),然后就可以把这个元素放到对应的位置中了。如果这个元素所在的位子上已经存放有其他元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。从HashMap
中get
元素时,首先计算key
的hashcode
,找到数组中对应位置的某一元素,然后通过key
的equals
方法在对应位置的链表中找到需要的元素。从这里我们可以想象得到,如果每个位置上的链表只有一个元素,那么HashMap
的get
效率将是最高的,但是理想总是美好的,现实总是有困难需要我们去克服,哈哈~
计算key的hashCode
我们可以看到在HashMap
中要找到某个元素,需要根据key
的hash
值来求得对应数组中的位置。如何计算这个位置就是hash
算法。前面说过HashMap
的数据结构是数组和链表的结合,所以我们当然希望这个HashMap
里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash
算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。
1
|
static int hash(int h) {
|
假设key.hashCode()
的值为:0x7FFFFFFF
,table.length
为默认值16
。
上面算法执行如下:
再调用indexFor(int h, int length)
方法最后返回的index为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
结果中的1、2、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)的算法,应该是考虑1、2、3三位出现重复位^运算的情况。使得最低位上原hashCode的8位都参与了^运算,所以在table.length为默认值16的情况下面,hashCode任意位的变化基本都能反应到最终hash table 定位算法中,这种情况下只有原hashCode第3位高1位变化不会反应到结果中,即:0x7FFFF7FF的i=15。
根据hashCode返回数组中位置
我们首先想到的就是把hashcode
对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,能不能找一种更快速,消耗更小的方式呢?java中时这样做的
1
|
static int indexFor(int h, int length) {
|
首先算得key
的hashcode
值,然后跟数组的长度-1
做一次“与”运算&
。看上去很简单,其实比较有玄机。比如数组的长度是2的4次方
,那么hashcode
就会和2的4次方-1
做“与”运算。很多人都有这个疑问,为什么HashMap
的数组初始化大小都是2的次方
大小时,HashMap
的效率最高,我以2的4次方
举例,来解释一下为什么数组大小为2的幂
时HashMap
访问的性能最高。
看下图,左边两组是数组长度为16
(2的4次方
),右边两组是数组长度为15
。两组的hashcode
均为8
和9
,但是很明显,当它们和1110
(1110
是数组长度15-1
所得到的2
进制)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8
和9
会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8
或者9
,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15
的时候,hashcode
的值会与1110
(1110
是数组长度15-1
的2
进制)进行“与”,那么最后一位永远是0
,而0001
,0011
,0101
,1001
,1011
,0111
,1101
这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!
所以说,当数组长度为2的n次幂
的时候,不同的key
算得的index
相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
说到这里,我们再回头看一下HashMap
中默认的数组大小是多少,查看源代码可以得知是16
,为什么是16
,而不是15
,也不是20
呢,看到上面的解释之后我们就清楚了吧,显然是因为16
是2的整数次幂
的原因,在小数据量的情况下16
比15
和20
更能减少key
之间的碰撞,而加快查询的效率。
所以,在存储大容量数据的时候,最好预先指定HashMap
的size
为2的整数次幂次方
。就算不指定的话,也会以大于且最接近指定值大小的2次幂
来初始化的,代码如下(HashMap
的构造方法中):
1
|
// Find a power of 2 >= initialCapacity
|
HashMap的resize
当HashMap
中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对HashMap
的数组进行扩容,数组扩容这个操作也会出现在ArrayList
中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,而在HashMap
数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize
。
那么HashMap
什么时候进行扩容呢?当HashMap
中的元素个数超过数组大小*loadFactor
时,就会进行数组扩容,loadFactor
的默认值为0.75
,也就是说,默认情况下,数组大小为16
,那么当HashMap
中元素个数超过16*0.75=12
的时候,就把数组的大小扩展为2*16=32
,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap
中元素的个数,那么预设元素的个数能够有效的提高HashMap
的性能。比如说,我们有1000
个元素HashMap
, 但是理论上来讲new HashMap(1024)
更合适,不过上面已经说过,即使是1000
,HashMap
也自动会将其设置为1024
。 但是new HashMap(1024)
还不是更合适的,因为0.75*1000
< 1000
, 也就是说为了让0.75 * size
> 1000
, 我们必须这样new HashMap(2048)
才最合适,既考虑了&
的问题,也避免了resize
的问题。
key的hashcode与equals方法改写
在第一部分HashMap
的数据结构中,就写了get
方法的过程:首先计算key
的hashcode
,找到数组中对应位置的某一元素,然后通过key
的equals
方法在对应位置的链表中找到需要的元素。所以,hashcode
与equals
方法对于找到对应元素是两个关键方法。HashMap
的key
可以是任何类型的对象,例如User
这种对象,为了保证两个具有相同属性的user
的hashcode
相同,我们就需要改写hashcode
方法,比方把hashcode
值的计算与User
对象的id
关联起来,那么只要user
对象拥有相同id,那么他们的hashcode
也能保持一致了,这样就可以找到在HashMap
数组中的位置了。如果这个位置上有多个元素,还需要用key
的equals
方法在对应位置的链表中找到需要的元素,所以只改写了hashcode
方法是不够的,equals
方法也是需要改写的~当然啦,按正常思维逻辑,equals
方法一般都会根据实际的业务内容来定义,例如根据user
对象的id
来判断两个user
是否相等。
在改写equals
方法的时候,需要满足以下三点:
- 自反性:就是说a.equals(a)必须为true。
- 对称性:就是说a.equals(b)=true的话,b.equals(a)也必须为true。
- 传递性:就是说a.equals(b)=true,并且b.equals(c)=true的话,a.equals(c)也必须为true。
通过改写key
对象的equals
和hashcode
方法,我们可以将任意的业务对象作为map
的key
(前提是你确实有这样的需要)。
原文作者:annegu(iteye作者)
原文标题:深入理解HashMap
原文作者:marystone(iteye作者)
原文标题:HashMap hash方法分析