第一篇文章传送门
第二篇文章传送门
第三篇文章传送门
为什么要扩容
当HashMap中的结点少时,进行查询是比较快的,但是当结点越来越多时,Hash表链化严重,复杂度由O(1)变为O(N),大大增加了查询的时间,通过扩容可以缓解该问题。
树化后的时间复杂度为O(log N)。
resize代码:
final HashMap.Node<K, V>[] resize() {
HashMap.Node<K, V>[] oldTab = this.table;
int oldCap = oldTab == null ? 0 : oldTab.length;
int oldThr = this.threshold;
int newThr = 0;
int newCap;
if (oldCap > 0) {
if (oldCap >= 1073741824) {
this.threshold = 2147483647;
return oldTab;
}
if ((newCap = oldCap << 1) < 1073741824 && oldCap >= 16) {
newThr = oldThr << 1;
}
}
else if (oldThr > 0) {
newCap = oldThr;
}
newCap = 16;
newThr = 12;
}
if (newThr == 0) {
float ft = (float)newCap * this.loadFactor;
newThr = newCap < 1073741824 && ft < 1.07374182E9F ? (int)ft : 2147483647;
}
this.threshold = newThr;
HashMap.Node<K, V>[] newTab = new HashMap.Node[newCap];
this.table = newTab;
if (oldTab != null) {
for(int j = 0; j < oldCap; ++j) {
HashMap.Node e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null) {
newTab[e.hash & newCap - 1] = e;
}
else if (e instanceof HashMap.TreeNode) {
((HashMap.TreeNode)e).split(this, newTab, j, oldCap);
}
else {
HashMap.Node<K, V> loHead = null;
HashMap.Node<K, V> loTail = null;
HashMap.Node<K, V> hiHead = null;
HashMap.Node hiTail = null;
//
HashMap.Node next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null) {
loHead = e;
} else {
loTail.next = e;
}
loTail = e;
} else {
if (hiTail == null) {
hiHead = e;
} else {
hiTail.next = e;
}
hiTail = e;
}
e = next;
} while(next != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
oldTab:引用扩容前的哈希表
oldCap:表示扩容前table数组的长度
oldThr:表示扩容之前的扩容阈值,
newThr:扩容之后的阈值
newCap:扩容之后table数组的大小
if (oldCap > 0) {
if (oldCap >= 1073741824) {
this.threshold = 2147483647;
return oldTab;
}
if ((newCap = oldCap << 1) < 1073741824 && oldCap >= 16) {
newThr = oldThr << 1;
}
}
oldCap是扩容前数组的大小,如果扩容前数组不为空,那么就说明散列表之前已经初始化过了,是一次正常的扩容。
如果扩容达到了HashMap最大阈值,那么下次的扩容条件就改为int的最大值。
正常情况下是不会达到最大阈值的,所以,当不达到最大阈值的时候,那么就将阈值翻一倍。二进制中向左移动一位就代表数字翻倍。
else if (oldThr > 0) {
newCap = oldThr;
}
oldCap既然有大于0的情况,那么就有等于0的情况,
当调用这两个构造方法时,会出现中两种情况。
//1、new HashMap(initCap,loadFactor)
//2、new HashMap(initCap)
当oldCap为0,说明HashMap为null,当oldThr大于0时,就将oldThr赋值给newCap。
else {//这是oldCap等于0的情况
newCap = 16;
newThr = 12;
}
当oldThr为0的时候,这个时候调用的构造方法中只有加载因子的参数。此时就将HashMap初始化,赋值给它长度和扩容阈值。
if (newThr == 0) {
float ft = (float)newCap * this.loadFactor;
newThr = newCap < 1073741824 && ft < 1.07374182E9F ? (int)ft : 2147483647;
}
this.threshold = newThr;
当newThr为0时,通过newCap和loadFactor计算出一个newThr。将newThr赋值给threshold。
上面一系列就是获取扩容之后数组的长度等。接下来就是如何处理原HashMap中的节点。
我们知道,在HashMao中,可以存在单个节点,也可以是链表的形式,更可以是树。需要判断节点的不同形式。
HashMap.Node<K, V>[] newTab = new HashMap.Node[newCap];
this.table = newTab;
if (oldTab != null) {
如果oldTab不为空,那么就表示扩容之前的table不为空。
for(int j = 0; j < oldCap; ++j) {
HashMap.Node e;
道
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null) {
newTab[e.hash & newCap - 1] = e;
}
e表示当前node的节点。
如果(e = oldTab[j]) != null) ,说明当前位置有数据,但是数据具体是单个数据还是链表数据或者是树,还不知道。
oldTab[j] = null;置为空方便JVM回收。
e.next == null) ;如果向下走不为空,那么就说明当前节点是单个数据。那么就找到在新的HashMap中的位置。
判断当前位置有没有被树化。
else if (e instanceof HashMap.TreeNode) {
((HashMap.TreeNode)e).split(this, newTab, j, oldCap);
}
没有被树化就代表是链表,毕竟单个节点和树化在前面已经判断过了。
设置低位链表和高位链表的头尾节点。
低位链表:存放在扩容之后的数组的下标,与当前数组的下标位置一致。
高链位表;存放在扩容之后的数组的下标位置为 当前数组下标位置+扩容之前数组的长度。
/
else {
HashMap.Node<K, V> loHead = null;
HashMap.Node<K, V> loTail = null;
HashMap.Node<K, V> hiHead = null;
HashMap.Node hiTail = null;
//
HashMap.Node next;
假如长度为默认的16,二进制为4位表示,扩容后用5位二进制进行表示,如果首位为0,表示为低链位表,
if ((e.hash & oldCap) == 0) {
if (loTail == null) {
loHead = e;
} else {
loTail.next = e;
}
loTail = e;
}
表示低链位表有数据,如果有数据,那么就将下一位置空,重新选择位置插入。
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}