HashMap的底层是以数组的方式进行存储。将key-value对作为数组中的一个元素进行存储;key-value是entry的属性,其中将key的值进行hash之后进行存储,即每一个key计算hash值,然后进行存储。每一个hash值都是对应一个数组的下标,这个下标是根据hash值与数组长度计算得来的;由于不同的key可能会有相同的hash,即该位置上可能会出现两个元素,对于这种情况,hashMap采用的是链表的形式进行存储的;
当向容器中put元素时,会判断当前容器元素的个数,如果大于等于阈值即当前数组的长度乘以加载因子的值的时候,就会自动扩容;扩容就要重新计算容量,向hashmap不断添加新元素,而hashMap内部的数组无法装在更多的元素时,hashmap就需要扩大数组的长度,以便能够装入更多的元素。当然java的数组是无法自动扩容的,方法就是用一个新的大容量数组代替容量小的数组。e.next = newTable[i]; newTable[i]=e;单链表的头插入方式,同一个位置新元素总是会被放在头部的位置,这样先放在一个索引上的元素会最终那会被放在entry链的尾部(如果发生hash冲突的话),在旧数组中同一条entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上。
在jdk1.8 我们使用的2次幂扩容,经过rehash之后,元素的位置要么是在原位置,要么是在原位置的基础上在移动2次幂的位置。我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。有一点注意区别,JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上图可以看出,JDK1.8不会倒置。在JDK1.8中发生hashmap扩容时,遍历hashmap每个bucket里的链表,每个链表可能会被拆分成两个链表,不需要移动的元素置入loHead为首的链表,需要移动的元素置入hiHead为首的链表,然后分别分配给老的buket和新的buket。
HashMap 扩容时对红黑树节点的修剪主要分两部分,先分类、再根据元素个数决定是还原成链表还是精简一下元素仍保留红黑树结构。
1.分类
指定位置、指定范围,让指定位置中的元素 (hash & bit) == 0 的,放到 lXXX 树中,不相等的放到 hXXX 树中。
2.根据元素个数决定处理情况
符合要求的元素(即 lXXX 树),在元素个数小于等于 6 时还原成链表,最后让哈希表中修剪的痛 tab[index] 指向 lXXX 树;在元素个数大于 6 时,还是用红黑树,只不过是修剪了下枝叶;
不符合要求的元素(即 hXXX 树)也是一样的操作,只不过最后它是放在了修剪范围外 tab[index + bit]。