HashMap浅析(一)
无论几年的程序猿,面试必问的题,HashMap算一大山,这座山翻不过去,离自己梦想的公司想必是无缘的。
很多人觉得会用HashMap不就行了吗,为什么非要研究它的原理呢?
摸一摸自己的大光头,你真的会用吗?如何用效率高?除了当普通的数据结构使用,其他地方可不可以用到呢?
接下来我会大致的对HashMap进行一个简短的分析。学习是一个循循渐进的过程,不要妄想一晚上学会什么原理,教你的是骗子,但你不能当傻子。
第一节是对于数组容量为2的幂次方的解惑。后续会慢慢补充
初始容量
HashMap的初始容量默认为 1<<4,即16
//The default initial capacity - MUST be a power of two.
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
位运算讲解
我先给大家介绍一下代码片段中的位运算符,方便不懂的小伙伴知道如何计算
首先敲出来1的二进制,养成良好的习惯,32位记得写全
位移前: 0000 0000 0000 0000 0000 0000 0000 0001
代码中的<<是左移运算符,即左移四位.
这里需要注意,不是单指左移二进制中的尾部1
位移中:0000 | 0000 0000 0000 0000 0000 0000 0001
如上面所示:你可以把那个“|”当作一个水龙头,左移四位,就是让水龙头把离它近的四位0排出去
位移中:| 0000 0000 0000 0000 0000 0000 0001
排出去四位0之后呢,平白无故的少了四位兄弟,那得补充上来0对吧(空位补零)
位移后:0000 0000 0000 0000 0000 0000 0001 0000
位移前:0000 0000 0000 0000 0000 0000 0000 0001
尽管看上去像只是1左移了四位,但是并不能这么想,如果是:…0001 0011 0001呢。
上面只是说了基础的算法,那如果我想快速运算呢,不想通过二进制呢?
x<<y 相当于
x ∗ y 2 x*y^2 x∗y2
x>>y相当于
x / 2 y x/2^y x/2y
回归正题
注意代码上方的注释:默认初始容量-必须是2的幂次方值
那么为什么一定要让容量为2的幂次方值呢,带着这个问题继续看下去。
首先HashMap的底层数据结构:数组+链表,当链表长度大于8时,转变为红黑树;这是常见的答案。
数组是什么数组?链表是什么链表?(皮一下很开心)
//这一行代码应该都能了解吧
transient Node<K,V>[] table;
众所周知啊,HashMap中的数组使用散列即常说的哈希算法确定角标,使用链地址法解决哈希冲突。
//jdk1.8之后的hash算法实现,1.7的indexFor已经去掉了,但是用法没变。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这里着重解释一下(h = key.hashCode()) ^ (h >>> 16)
是不是看着很懵?懵就对啦
下面举个例子:比如计算key为“程序猿”的值,hashcode值:30804443
代码中“>>>”是无符号右移,即逻辑右移,按照我上面位运算的讲解,这个时候是需要整体向右移动16位,
即图中黑竖线的右边数字全部舍弃掉。
然后老规矩,空出来的位置补0
java中的^运算是异或,相同为0,不同为1的运算
可以看到通过这样的运算,高16位其实没变化。
说的直白点,这么多步的运算,其实就是为了让key的hashCode值进行一个自身的高16位与低16位的异或运算,可以有效减少hash碰撞。
到这里其实只是算出来了HashMap中的hash值,接下来还有一个最常问到的代码片段
(n - 1) & hash
n是数组长度,hash是上面hash()方法算出来的hash值。
这里可以试着想一想,n是2的幂次方,也就是2,4,8,16等等等,他们的二进制有什么特点?
额,看起来并不明显是吧,接下来再看看n-1的呢
低位都是1,对吧,接下来结合起来看吧!
首先解释下&运算是按位与,即0&0=0,0&1=0,1&0=0,1&1=1.
那么想一想上面计算得出的hash值,两个进行按位与计算会是什么样的呢?
以n=16为例
看出来了吗,最终结果只跟hash值的最后几位有关系!!!
长度采取2的幂次方的原因
首先假设现在数组长度非2的幂次方,比如n=10,(n-1)=9,它的二进制为:1001,
现在有一个key=“程序猿”,通过上述的hash()方法计算出来的二进制的最后四位:1101,
1001&1101=1001
1001即得到的index值,但是换个思路想想,在保证n-1值(1001)不变的情况下,是不是还有另外的值会得到1001?当然是有的:1011.
而且还存在某些index值永远不会用到,比如0111永远不会出现。
结论:如果在不规定n为2的幂次方情况下,很容易出现数组下角标index重复(出现重复的概率提升)或者存在某些index永远用不到的情况,所以在n为2的幂次方时,只要保证hash()算法本身分布均匀就足够了。
还有一个重要原因:在n为2的幂次方情况下,hash%n等价于(n-1)&hash,而且"&“运算要比”%"快得多。