下面讲解一下常用的map-->hashMap。由于本人经验不足,可能导致有些地方讲错,希望各位能够指出。我还是喜欢从常用的方法入手剖析源码。
1.首先进入一下无参构造器。
就只是仅仅做了一个将负载因子默认为0.75这一步骤。
2.put(key,value)方法
这个步骤是比较复杂的一部分,涉及的源码较多,并且讲解也较多。
(1)首先看一下hash(key)这个方法
这个地方可谓是关键算法之一。意思是将key的hasCode值和h(临时储存hashCode值)的高16位全变为0,然后两者进行异或运算。这样就能尽量避免碰撞的发生了。
(2)接着进入putVal(hash,key,value,false,true)方法
由于源码过长,我们分步骤来讲解。
在哈希表还没真正初始化的时候,首次调用put(key,value)方法的话,会先进行初始化。
先大致看一下初始化时调用过的resize()的一些重要的东西。
好了,大致上就经历了上面三段代码。其实就是对于Node[] table的初始化,长度是16,扩容阈值是12。(这里有一个疑问,网上很多博客都说的是,桶的数量在12的时候就会进行扩容,但是我根据代码的逻辑判断,觉得是所有元素总量超过阈值就会扩容,可是这样在一定程度上也不合理,但是我还是觉得自己的想法是正确的。)
然后进入第二个if判断,i=(n-1)&hash 这个式子也相当重要,因为hash经过计算过后,仍然是远大于数组的长度的,那么这个时候,可以进行 取余运算,(这个式子也解释了为什么hashMap的容量必须是2的n次方)上面的式子其实就是取余,只是效率更加的高了。这个时候,只要数组的当前位置为null那么就放入。然后修改次数++,判断一下是否需要扩容。这就是第一次初始化的过程了。后面将围绕put和resize详细讲解。
好了,开始进行更加深入的讲解。
put的第一种情况:put了一个键值对,对应的位置上面是存在元素的。那么将进入这段代码。
(1)会先判断原本数组位置上的p对应的key是否与插入的key相同。这里采用==和equal()方法,这也可能是因为map常选择string的原因。如果是相同的key的话,那么直接替换掉原本的key对应的value,并返回原本的value。
(2)如果是树的话,插入节点或者替换节点上的数据,因为本人不太擅长树的编写,只能够大致原理,所以就不献丑了,哈哈。
(3)如果p这个位置上没有的话,那么就只能在链表里了,链表里分了两种情况。1.存在,就替换。2.不存在,就放入链尾,并且验证是否超过了链表转换为树的阈值8,超过的话,需要将节点转换为红黑树。
put的第二种情况:put的键值对对应的数组的位置上是没有元素的,并且刚好达到了扩容的要求,也就是这一段代码。
那么这个时候,就需要去关心resize()了。
3.resize()方法。
这就是扩容机制,容量变为两倍,并且阈值也变为两倍。
接着看一下扩容后数组的处理。代码有点长,分为两部分。
这里有个循环,就是判断链表是否走完。e.hash&oldCap也是相当经典的。用于判断链表对应的节点是否需要移动。&oldCap也就是将e.hash在oldCap的高位之前的全部变为0,然后判断剩余的和oldCap的大小,如果大于等于的话,那么得到的就是oldCap,那么就需要移动位置,移动到原本位置加上oldCap,如果小于的话,那么就保持位置不动
4.get(key)
步骤如下:
(1)计算key的哈希值,然后进入getNode(hash,key)
5.remove(key)
也就是和put是类似的,分析一下大致步骤,分为四种情形。
(1)数组位置没有key
(2)数组上的第一个key刚好相同,那么这个时候就赋值给临时变量e
(3)从红黑树中寻找。
(4)从链表中寻找。
针对上面的情况。
(1)返回null
(2)将该节点的下一节点放到表的位置上。返回该value
(3)从树中移除,可以看见,传入了tab这个变量,我认为如果长度不到8,那么会变为链表(以后验证)返回该value。
(4)将上一节点的next与该节点的next相连,返回该value
6.接着讲一下Map的遍历。
其遍历过程如上。一步一步分析。其实map的遍历已经被用于set的遍历上了。当我分析了set的遍历之后,会将地址贴过来