之前在网上看到的一个面试题,觉得挺有意思的,找了相关资料,看了HashMap的源码,今天有时间,写上来。(JDK版本1.9,其他版本的源码可能有出入)
首先来看HashMap中的一段代码
注释就说的很清楚了,默认的初始容量 - 必须是2的幂。也就是说,HashMap的长度自己定义的时候,只要是2的次幂就行。那么为什么要是2的次幂?3的次幂行不行?我们接着往下看。
来看HashMap的put方法:
可以看到put方法调用了putVal方法,再来看putVal方法,以下为putVal方法的部分源码:
红圈标注的就是在计算存储的值要存放在tab数组的位置,也就是这段算法决定了为什么HashMap的长度要是2的次幂。
n是什么?
我们不妨先来看这段代码中的resize方法做了什么
下面是resize方法的部分源码,从注释就可以看出,是用来初始化或扩容数组的
那么显而易见,tab就是HashMap底层存储数据的数组。n就是tab数组的长度。
hash是什么?
put方法在调用putVal方法的时候,传参数时,调用了hash方法,下面是hash方法的源码
hash方法计算了key的hash值。
ok,所有变量的用途都搞清楚了,我们还是接着来看这段代码:
我们假设数组长度分别为15和16(也就是n=15 或 n=16),假设两个值的hash码分别为8和9,那么&运算后的结果如下:
从上面的例子中可以看出:当它们和15-1(1110)“与”运算的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了哈希碰撞,hash码是8和9的值,被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链表,得到hash码是8或9的值,这样就降低了查询的效率。
同时,我们也可以发现,当数组长度为15的时候,hash值会与15-1(1110)进行“与”运算,15-1(1110)永远是0,想0001,0011,0101,1001,1011…,这些位置永远都存放不了元素,空间浪费相当大,更严重的是,数组可以使用的位置比数组实际长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!
而当数组长度为16时,即为2的n次方时,2n-1得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash()方法对key的hashCode的优化,使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。
所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
可能有人想问,那我自己定义HashMap的长度呢?我把长度写成其他数字,比如这样:
不用担心,你能想到的,官方也想到了。我们来看HashMap的构造方法:
可以看出它调用了这个构造:
参数一:你定义的长度
参数二:加载因子,默认是0.75
黄色方框不用多说,当你定义的值超过HashMap的最大容量时,使用最大容量
红色方框,我们来看tableSizeFor(int cap)方法,做了什么,下面是该方法的源码:
我们可以运行这段代码,可以发现当你传的9进来后,会帮你优化为16
继续运行这段代码,会发现如果传3会优化为4,传5会优化为8,以此类推,都会优化为2的次幂。
传负数、0、1,会被优化为1。
ok,以上