Hashmap是一种非常常用的、应用广泛的数据类型,他实际上是一个数组和链表的结合体(在数据结构中,一般称之为“链表散列“)
结构如下面图中所示
transient Entry[] table;
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
final int hash;
Entry<K,V> next;
..........
}
上面的Entry就是数组中的元素,它持有一个指向下一个元素的引用,这就构成了链表。
当我们往hashmap中put元素的时候,先根据key的hash值得到这个元素在数组中的位置(即下标),然后就可以把这个元素放到对应的位置中了。如果这个元素所在的位子上已经存放有其他元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。从hashmap中get元素时,首先计算key的hashcode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。从这里我们可以想象得到,如果每个位置上的链表只有一个元素,那么hashmap的get效率将是最高的,但基本上都不会这么好。
以上是hashmap的结构,接下来是hash算法:
在hashmap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过hashmap的数据结构是数组和链表的结合,所以我们当然希望这个hashmap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。
所以我们首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。
但是计算机最讨厌的就是取模和除法,因为“模”运算的消耗还是比较大的,所以得找一种更快速,消耗更小的方式。
重点来了,java中时这样做的,
static int indexFor(int h, int length) {
return h & (length-1);
}
首先算得key得hashcode值 h,然后跟数组的长度length-1做一次“与”运算(&)。看上去很简单,其实比较有玄机。比如数组的长度是2的4次方,那么hashcode就会和2的4次方-1做“与”运算。很多人都有这个疑问,为什么hashmap的数组初始化大小都是2的次方大小时,hashmap的效率最高。
因为length=2^n对应的二进制中只有一个位为1,如果直接计算h & length,那么只能得到两种结果(2^n或0),而length-1=2^n-1对应的二进制中末尾有n个位都是1。计算h &( length-1)其实等价于计算h%length,本质就是截取h的后n个二进制位,目的是将我们put进来的元素的key映射到HashMap底层数组对应的hash桶中,这里采用与运算比模运算的开销小得多,由于indexFor函数经常要使用,所以为了效率HashMap要求底层数组的length总是2的幂,这样就能用与运算代替模运算了。这就是h & (length-1)的玄机。
简单来说就是,hashcode对数组长度取模运算可以使元素的分布相对来说是比较均匀,而取模运算开销大,所以就用h&(length-1),h%length等于h&(length-1),而前提是length是2的次方数
说到这里,再回头看一下hashmap源代码中默认的数组大小是多少,查看源代码可以得知是16
所以,在存储大容量数据的时候,最好预先指定hashmap的size为2的整数次幂次方。就算不指定的话,也会以大于且最接近指定值大小的2次幂来初始化的,代码如下(HashMap的构造方法中)
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
还有一个值得注意的问题:
当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中(在上一篇ArrayList的源代码中有grow方法进行扩容),所以这是一个通用的操作,而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小loadFactor负载因子时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。