/**
* 重新hash方法,对高16位和低16位进行异或操作,因为在计算hash槽索引位置的时候,基本都是低16位参与运算,所以这样做可以降低hash冲突的概率。
**/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* 1. 找到key所在的hash槽位置
* 2. 没有hash冲突的情况直接插入
* 3. 存在hash冲突则使用链表解决hash冲突,遍历整个链表判断如果相同的key就更新,没有相同的就继续挂链表
* 4. 如果链表长度大于等于8,将链表转为红黑树,提升hash冲突时的查询速度
* 5. 如果hashMap大小超过阈值(size * 0.75),对hash槽进行扩容
**/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果创建hashMap的时候没有指定大小,通过resize()设置容量默认16。
if ((tab = table) == null || (n = tab.length) == 0){
n = (tab = resize()).length;
}
//对key的hash值取模,找到hash槽的索引下表
if ((p = tab[i = (n - 1) & hash]) == null)
//当前hash槽内没有数据,创建一个Node节点放进去
tab[i] = newNode(hash, key, value, null);
else {
//当前hash槽内有数据,使用链表解决hash冲突
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//如果当前hash槽内节点的key与插入数据的key相同,就直接将节点内的值进行更新,然后返回旧值
e = p;
else if (p instanceof TreeNode)
//如果当前节点已经转成红黑树,则使用红黑树的方式插入新的数据
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//当前槽内节点既不是红黑树,key值也不喝插入的相同,则使用双向的方式解决hash冲突
for (int binCount = 0; ; ++binCount) {
//依此遍历整个链表,如果当前是链表的最后一个节点,就创建一个node挂到最后面,然后返回一个null回去
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//判断链表长度是否大于等于8,则将链表转红黑树
if (binCount >= TREEIFY_THRESHOLD - 1){ // -1 for 1st
treeifyBin(tab, hash);
}
break;
}
//判断下一个节点的key是否和当前插入的值相同,如果相同直接更新,并且返回旧值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//上面两个条件都不满足,说明还没遍历到最后一个节点,并且当前节点的key与插入的key不通,准备继续遍历下一个节点
p = e;
}
}
//e就代表被更新的节点,经过上面那些操作,如果key在插入这条数据之前就已经存在了,那么就走下面这个if分支
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//以下代码,只有在插入一个新的节点时才会执行
//用来在迭代的时候进行failfast的一个变量,每个更新插入数据都会累加1
++modCount;
//如果大小超过了阈值(hash槽长度*0.75,进行hash扩容)
if (++size > threshold)
resize();
//一个埋点方法,便于子类集成hashmap做后续的操作
afterNodeInsertion(evict);
//新插入数据的时候就返回一个null
return null;
}
/**
* 两重情况会调用此方法:
* 1. hashMap大小超过阈值,对其进行扩容,并且对hash冲突的节点重新分配hash槽
* 2. 初始化的时候会通过此方法对hashMap设置默认容量16
**/
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分支
if (oldCap > 0) {
//如果hashMap容量已经超过最大值,就不扩容拉,直接返回
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//hashMap特性,每次扩容一倍,这样扩容之后,hash冲突的节点可以均匀分散在扩容之后的hash槽内
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//创建hashMap并且设置了容量,在初始化的时候会调用
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//创建hashMap没有设置容量,在初始化的时候会调用
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//创建hashMap并且设置了容量,在第一次插入数据的时候会设置阈值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//更新扩容or初始化之后的阈值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//根据扩容之后的大小,创建新的hash槽数据
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//扩容的时候会进入以下分支, 把之前的所有node节点rehash之后放到扩容之后的hash槽上。
if (oldTab != null) {
//遍历所有hash槽
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//当前hash槽内只有一个节点,直接rehash
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//红黑树的情况,特殊处理
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//对该hash槽内的链表进行遍历
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;
//这里就是将一个链表拆成两个链表,但是这个判断是不是让人很疑惑?
//其实很加单,其实就是将链表拆分之后,分别将整颗链表放入扩容之后对应的hash槽内,加入扩容之前长度是16,
//index=4的位置有长度为4的链表,在扩容之后,这个链表的节点在新hash槽内的索引位置只能是4或者4+16=20。
//当初这四个节点进入同一个hash槽内说明他们的hash值位与00001111的时候值肯定都相同,扩容之后与之前的计算唯一的差别就是最左边多了一个1,即00011111,
//而16的二进制为00010000,所以直接让hash值位与老容量,就可以知道node在新hash槽内的索引位置了,同一个hash槽内的节点用链表串起来。
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);
//这里就是将两个拆分后的两个链表,分别放入新的hash槽内,两个hash槽索引位置相差扩容容量的大小,即16
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}