目录
一开始创建HashMap的时候肯定是未初始化的,所以查看一下具体的resize()的实现逻辑
然后,它会保存旧的table数组、旧的容量oldCap和阈值oldThr。然后,根据旧的容量来判断是否需要进行扩容操作。
之后要进行的就是将元素重新分配到table(哈希表)数组当中
7、调用afterNodeInsertion()方法进行插入操作后的处理。
按住Ctrl点击put方法进去到源码当中(结果如下图)
点进hash()方法查看细节
在传入key之后进入了hash方法,在方法中定义了一个整型h(用于存储最终的哈希值)。
(key == null) ? 0 : (h = key.hashCode())
这里是一个三元运算符,代表的意义为,检查传入的key是否为null,如果是null则直接返回哈希值0,否则继续执行下面的步骤
(h = key.hashCode())
这里调用了对象key的hashCode方法,获取到了它的哈希码,然后将结果返回给了h
再来看(h >>> 16);
这里使用了无符号右移16位,相当于把 h
的二进制表示向右截断 16 位,只保留低 16 位的值(扰动),从而增加哈希值的随机性减少冲突
(h = key.hashCode()) ^ (h >>> 16)
然后执行异或运算得到最终的哈希值
然后回到这里,点进putVal方法
先展示整段代码,接下来逐步进行解析
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
首先来看
Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
这段代码主要处理数组的初始化和扩容操作。
-
首先,定义了一系列局部变量:
tab
(存储哈希表的数组),p
(当前节点),n
(哈希表容量),i
(数组索引)。 -
tab = table
将哈希表的数组赋值给tab
变量。 -
检查数组是否为 null 或者长度为 0。如果是,则说明哈希表还没有被初始化或者已经为空,需要进行初始化或者扩容操作。
-
在括号中的部分
(tab = resize())
是调用了resize()
方法进行数组的初始化或者扩容操作,并将返回的数组赋值给tab
变量。 -
然后,将
tab.length
赋值给n
变量,这里的n
表示数组的长度。
一开始创建HashMap的时候肯定是未初始化的,所以查看一下具体的resize()的实现逻辑
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> 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;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
以上就是resize()方法的完整代码,其作用主要就是对HashMap进行扩容。
Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0;
首先对变量进行了定义
1、代码创建一个变量oldTab,并将其赋值为table数组。table数组是HashMap中存储元素的数组。
2、代码使用三元运算符判断oldTab是否为null,如果是null,则表示原来的HashMap没有任何元素,将旧的容量oldCap设置为0;否则,将旧的容量oldCap设置为oldTab数组的长度。
3、代码将阈值threshold赋值给变量oldThr。threshold是HashMap中的一个属性,表示数组需要进行扩容的阈值。
4、定义了两个变量newCap和newThr,并将newThr初始化为0。
然后,它会保存旧的table数组、旧的容量oldCap和阈值oldThr。然后,根据旧的容量来判断是否需要进行扩容操作。
我们先来看这一部分的代码,这是一开始未初始化HashMap的时候,就走的这块
1、 旧的容量(oldCap)为0,这表示原来的HashMap没有任何元素。在这种情况下,代码会将新的容量(newCap)设置为默认的初始容量(DEFAULT_INITIAL_CAPACITY——16),并且计算新的阈值(newThr)。新的阈值是根据新容量和负载因子(DEFAULT_LOAD_FACTOR——0.75)计算得到的。
2、如果新的阈值(newThr)为0,说明需要重新计算新的阈值。代码中使用了float类型的变量ft存储新容量(newCap)乘以负载因子(loadFactor)的结果。然后检查新容量是否小于最大容量(MAXIMUM_CAPACITY)
3、并且检查ft是否小于最大容量(MAXIMUM_CAPACITY),如果满足条件,则将ft转换为int类型作为新的阈值(newThr),否则将新阈值(newThr)设置为Integer.MAX_VALUE,即最大的整数值。
4、然后,将新的阈值(newThr)赋值给HashMap的阈值(threshold)。
5、接下来,代码创建一个新的table数组(newTab)来代替旧的table数组,这里使用了类型转换和泛型抑制警告。
6、最后,将新的table数组(newTab)赋值给HashMap的table属性,完成了HashMap的扩容操作。
如果已经初始化了在resize()中就走的是这块
首先,代码判断旧的容量(oldCap)是否大于0。如果旧的容量大于0,说明原来的HashMap已经有元素存在。在这种情况下,代码会进行进一步的判断和处理。
如果旧的容量(oldCap)达到了HashMap的最大容量(MAXIMUM_CAPACITY),即oldCap >= MAXIMUM_CAPACITY,那么将阈值(threshold)设置为Integer.MAX_VALUE,即不再进行扩容操作,并直接返回旧的table数组(oldTab)。
如果旧的容量(oldCap)小于最大容量(MAXIMUM_CAPACITY),则将新的容量(newCap)设置为旧的容量的两倍(oldCap << 1),并且进行额外的判断。
首先,判断新的容量(newCap)是否小于最大容量(MAXIMUM_CAPACITY),以及旧的容量(oldCap)是否大于等于默认的初始容量(DEFAULT_INITIAL_CAPACITY)。如果这两个条件都满足,则将新的阈值(newThr)设置为旧的阈值(oldThr)的两倍(oldThr << 1),也就是将阈值翻倍。
接着,如果旧的容量(oldCap)为0,说明原来的HashMap没有任何元素,只有初始阈值(oldThr)大于0时,才进行以下操作:将新容量(newCap)设置为初始阈值(oldThr)。
作用:根据旧的容量、阈值和默认值来确定新的容量和阈值。如果旧的容量达到最大值,则直接返回旧的table数组。否则,根据条件判断来决定是否将新的容量扩大一倍,并且将新的阈值设置为旧的阈值的两倍。
之后要进行的就是将元素重新分配到table(哈希表)数组当中
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> 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;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
首先,检查旧的table数组是否为null,并进入循环。
循环内部,获取旧table数组中索引位置j处的元素e。
接下来,根据e的情况进行不同的操作
1、如果e是一个单节点(没有后续节点)
- 将旧table数组中索引位置j处的元素设为null。
- 将该元素根据其哈希值计算新table数组的索引,并将其放入新table数组中对应位置。
2、如果e是一个树节点(TreeNode)
- 调用
split()
方法将树节点e及其子节点按照哈希值重新分配到新的table数组中。
3、如果e是一个链表节点
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> 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;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
- 创建四个辅助节点:loHead、loTail、hiHead和hiTail。
- 使用do-while循环遍历链表,并根据e的哈希值的最低位(oldCap的二进制表示为100...00)将e放入低位链表(loHead和loTail)或高位链表(hiHead和hiTail)中。
- 循环结束后,将低位链表的头节点放入新table数组中的索引j处,将高位链表的头节点放入新table数组中的索引j+oldCap处。
if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; }while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; }
如果 (e.hash & oldCap) == 0,将其填入到低链表当中
- 如果低位链表的尾节点loTail为null,即低位链表还没有节点,则将当前节点e赋值给低位链表的头节点loHead。
- 如果低位链表的尾节点loTail不为null,即低位链表已经有节点,则将当前节点e连接到loTail的后面,即loTail.next = e。
- 最后,将低位链表的尾节点更新为当前节点e,即loTail = e。
else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; }if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; }
- 如果高位链表的尾节点hiTail为null,即高位链表还没有节点,则将当前节点e赋值给高位链表的头节点hiHead。
- 如果高位链表的尾节点hiTail不为null,即高位链表已经有节点,则将当前节点e连接到hiTail的后面,即hiTail.next = e。
- 最后,将高位链表的尾节点更新为当前节点e,即hiTail = e。
然后,判断高位链表是否非空,即判断hiTail是否为null。如果高位链表非空,则将高位链表的尾节点hiTail的next指针置为null,表示链表结束。然后,将高位链表的头节点hiHead放入新table数组的索引j + oldCap处,即newTab[j + oldCap] = hiHead。
至此,resize()方法就讲解完毕
回到putVal方法中来
1、首先定义一系列变量
Node<K,V>[] tab; Node<K,V> p; int n, i;
tab
表示哈希表数组,p
表示当前节点,n
表示哈希表长度,i
表示计算出的插入位置索引。
2、检查哈希表是否为空或长度为0
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
如果table
(哈希表)为空或长度为0,则通过调用resize()
方法进行初始化或扩容,并将返回的新数组赋值给tab
。然后,更新n
为新表的长度。
3、计算插入位置的索引
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
通过计算(n - 1) & hash
来确定插入位置的索引,并将索引赋值给i
。然后,检查位置tab[i]
是否为空,如果为空,则在该位置创建一个新的节点,并将其插入。
4、处理冲突情况(当计算出的位置不为空的时候)
else {
Node<K,V> e; K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// ...
}
如果位置tab[i]
不为空,则表示发生了冲突。首先,定义了变量e
表示当前节点,k
表示当前节点的键。
- 如果当前节点
p
的哈希值和键与待插入的键值对匹配,则将当前节点赋值给e
。 - 如果当前节点
p
是树节点(通过instanceof
判断),则将插入操作委托给树节点自身的putTreeVal()
方法进行处理。 - 如果当前节点
p
是普通链表节点,则通过循环遍历链表,寻找与待插入键值对匹配的节点:- 如果找到了匹配的节点
e
,则跳出循环。 - 如果未找到匹配节点,则将新创建的节点插入到链表尾部,并根据链表长度是否达到阈值(TREEIFY_THRESHOLD——8),判断是否需要将链表转换为红黑树。
- 如果找到了匹配的节点
5、更新已存在的键值对或插入新键值对
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
如果变量e
不为空,则表示已存在相同的键值对。如果onlyIfAbsent
标志为false
或旧值oldValue
为空,则更新该节点的值为新值value
。然后,调用afterNodeAccess()
方法进行额外处理,并返回旧值oldValue
。
6、更新哈希表状态和扩容(先插入后扩容)
++modCount;
if (++size > threshold)
resize();
modCount
计数加一,并判断当前大小是否超过阈值。如果超过阈值,则调用resize()
方法进行动态扩容。