1.为什么HashMap数组的长度得是2的N次方?
2的N次方对应的二进制(高位未补零) | (2的N次方)-1 对应的二进制(高位未补零) | |
N=1 | 10 | 1 |
N=2 | 100 | 11 |
N=3 | 1000 | 111 |
N=4 | 10000 | 1111 |
...... |
HashMap使用到的数据结构有数组、链表、红黑树。HashMap存储的键值对要存储在数组的哪个下标位置呢,是通过对2的N次方-1、key的hash值做与运算。拿N=4举例,那就是对1111和key的hash值做与运算,与运算的计算结果范围是[0000,1111],也就是十进制的[0,15],不会发生下标越界。
如果写代码时指定的长度不是2的N次方,例如,Mapmap = new HashMap<>(13);那么HashMap实际创建数组的长度是大于13的2的N次方值,也就是16。
那为什么不用取余来计算下标值呢?例如数组长度16,key的hash值和15求余,这样算出来的值也都属于[0,15]这个范围,是因为与运算的效率高于求余运算。
从上面可知,当向map put一个键值对的时候,会计算要保存在哪一个下标位置,例如要存储在下标为i的位置。我们知道两个数据可能值不相同但是hash值相同,因此需要判断下标i的位置是否已有键值对存储在这里了。
1、数组下标i的位置没有存储数据,那么就直接保存在数组下标i这个位置。
2、数组下标i的位置已经存储数据了。
1)判断当前存储的这个键值对和需要保存的键值对的key是否相同。
a)相同,覆盖旧的value。
b)不相同
判断是否节点是否是TreeNode
是:说明是红黑树结构 // todo,待补充
否:说明是链表结构,循环链表,当前键值对的key是否和链表中某个Node的key相同,如果相同,覆盖旧的value;如果和链表中所有Node的key都不相同,找到链表的尾节点,将当前键值对封装成Node插入链表尾部(尾插法)。如果不算当前节点,链表长度已经是8,算上当前节点,链表长度是9,就大于链表树化成红黑树的阈值8,那么就要把这个9个节点的链表改成红黑树了。这个过程是先遍历再添加再改成红黑树。
为什么要将链表改成红黑树呢?因为链表太长了的话,get和put都会遍历链表,效率低。
当链表长度=8时:
1、数组的长度<64,进行扩容(resize,数组长度扩大一倍),将一个链表拆分成两个短链表。
2、数组的长度>=64,不扩容,而是将链表改成红黑树。
将链表改成红黑树,会遍历链表,根据链表的Node,生成对应的TreeNode(是双向链表。和单向链表相比,删除更方便)。再将双向链表改成红黑树,首先将第一个节点作为红黑树的根节点,然后依次判断下一个节点应该处于红黑树的哪个位置(根据红黑树的特点)。然后将数组存储的链表节点的引用修改成红黑树节点的引用,链表就会被垃圾回收。
扩容就是把旧数组的所有节点搬到新数组的过程,新数组的长度是旧数组长度的2倍。
计算应该位于新数组的哪个下标位置呢?
计算方法仍然是对key的hash值和2的N次方-1做与运算(此时N的值是旧数组N的值+1)。
原因举例说明:
假如旧数组的长度是16,则新数组的长度是32。15对应的二进制是1111,31对应的二进制是11111。
旧的下标值 = key的hash值 & 01111。
新的下标值 = key的hash值 & 11111。
新下标值只有两种情况:
1、新下标值=旧下标值。(key的hash值红色对应位是0)
2、新下标值=旧下标值+旧数组的长度。(key的hash值红色对应位是1)
key的hash值红色对应位是0还是1呢?可以将key的hash值和16(本例中是16,对应二进制是10000)做与运算,如果运算结果是1,则红色对应位是1;如果运算结果是0,红色对应位是0。
数组存储的四种情况:
1、null,该下标没有存储元素。
2、存了一个键值对。
3、存的是一个链表。
4、存的是一个红黑树。
针对上面四种情况的搬家:
1、无需操作。
2、重新计算新的数组下标。
3、重新计算节点新的数组下标,链表中的节点新的下标值有两种情况(上面已经说明),因此就将这个一个链表拆分成了两个链表。将新数组的对应下标分别指向这两个链表。
4、重新计算节点新的数组下标。同理,新的下标值有两种情况。举例:如果搬家前红黑树共10个节点,7个节点要搬到新数组的高位,3个节点要搬家到新数组的低位,由于3<6(6是红黑树要退化成链表的阈值),搬到新数组的就是一个红黑树加一个链表。根据红黑树退化成链表的阈值可以知道,一个红黑树,搬过来有三种情况:1)两个链表。2)一个链表+一个红黑树。3)一个红黑树。