}
//如果e不为null
//就代表有节点的key与插入的键值对的key相同
//需要进行替换
//因为如果不需要进行替换,就代表没有相同key
//也就是上面的for循环遍历到了尾节点了
//遍历到尾结点的条件是e == null
if (e != null) { // existing mapping for key
V oldValue = e.value;
//先判断是否关闭了onlyIfAbsent
//再判断旧的值是否为null
//所以onlyIfAbsent其实是用来控制发生key冲突是否替换值的
if (!onlyIfAbsent || oldValue == null)
//替换为新值
e.value = value;
//调用afterNodeAccess方法
afterNodeAccess(e);
//返回旧值
return oldValue;
}
}
//版本进行更新
++modCount;
//判断是否需要扩容
//注意,这里是先新增键值对然后再扩容
//而jdk1.7是先进行扩容再新增键值对
if (++size > threshold)
resize();
//调用afterNodeInsertion方法
//evict是另一个false
afterNodeInsertion(evict);
return null;
}
可以看到这个putValue挺复杂的,步骤如下
-
先判断底层数组是否为null或为一个空数组,这是针对第一次的put
-
如果是,要进行第一次的扩容
-
计算哈希值获取对应的索引值,判断底层数组该索引是否已经存有对应键值对
-
如果没有,调用newNode方法进行插入
-
如果有,就代表发生了哈希冲突
-
先判断第一个位置,也就是数组里面存储的键值对的哈希值或者key是否和插入的键值对相等
-
如果相等,记录这个节点
-
如果不相等,可能下面的节点会发生key相等
-
判断第一个节点是不是树节点(使用instanceof),如果是就调用putTreeVal方法
-
如果不是就代表是链表形式,遍历链表来让每个节点hash值和key与插入键值对进行对比,同时记录链表的元素个数
-
先判断是否达到树化的计数阈值,如果达到,进行treeifyBin,跳出循环
-
判断是否出现相同的key,如果出现,跳出循环
-
判断替换节点是否为null
-
如果不为null,再判断是否关闭了onlyIfAbsent,或者旧的value是否为null
-
如果关闭了onlyIfAbsent和旧的value也不为null,就进行值替换为新值,而key不变
-
自增modCount,版本数加1
-
size自增,元素数量加一
-
判断size是否大于扩容阈值,如果大于进行resize扩容**(与jdk1.7不同,jdk1.7是扩容了之后再进行新增键值对,而jdk1.8是新增了键值对之后再扩容)**
treeIfBin
接下来我们看是如何进行树化的
源码如下
步骤如下:
- 判断底层数组是否为空,接下来判断底层数组的长度是否满足最小的树化计数阈值MIN_TREEIFY_CAPACITY
- 如果不满足,单纯进行扩容,此时树化的操作可能在扩容操作里面实现
- 如果满足树化条件(底层数组的长度满足最小的树化计数阈值)
-
计算出需要树化的链表所在的索引值
-
循环将Node结点转化为TreeNode(对应的是replacementTreeNode,其实这个方法就是简单地使用Node的信息去生成一个TreeNode)
-
这里循环的操作是生成了一个双向链表,采用尾插法,并且hd记录了链表的头结点
-
最后判断头结点是否为空,也就是这个双向链表是否为空
-
如果不为空,对底层数组执行treeify操作
所以,treeIfBin这个方法是让不满足树化条件的链表进行扩容,满足树化条件的链表变成一个双向链表
这里为什么要生成一个双向链表呢???
treeify
接下来我们看看真正的树化操作
可以看到,树化操作是使用链表来生成红黑树的
/**
-
Forms tree of the nodes linked from this node.
-
@return root of tree
*/
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;😉 {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}
resize
接下来我们看看是如何resize扩容的
源码如下
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//获取当前数组的容量
//并且针对第一次扩容,将容量设为0
//如果是无参构造,oldTab是为null的
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//记录当前扩容阈值
//如果是第一次扩容,这个阈值就是数组的容量
//如果是无参构造,这个threshold为0
int oldThr = threshold;
//开两个变量来记录新的容量和新的扩容阈值
int newCap, newThr = 0;
//如果旧的容量大于0,代表不是第一次扩容
if (oldCap > 0) {
///先判断旧的容量是否已经是允许的最大值
if (oldCap >= MAXIMUM_CAPACITY) {
//如果已经是最大值,就不进行扩容了
//不过要改变扩容阈值为Integer的最大值
//因为是Integer的最大值,而MAXIMUM_CAPACITY小于Integer的最大值
//下次就不会触发扩容
threshold = Integer.MAX_VALUE;
//因为不进行扩容,直接返回旧的数组
return oldTab;
}
//如果旧的容量没有超过允许的最大值,那么就可以进行扩容
//同理,扩容的规则为原来的两倍
//扩容完后,判断是否为允许的数组最大值,
//而且还要判断旧的容量是否要大于等于默认容量
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//因为新容量为旧容量两倍
//所以扩容阈值也相应变为2倍
newThr = oldThr << 1; // double threshold
}
//如果旧的容量不大于0,也就是为0
//而且旧的扩容阈值不为0
//代表的也是第一次插入
//注意,构造方法里面是将初始化容量存储进threshole的
else if (oldThr > 0) // initial capacity was placed in threshold
//所以,这里只需要让初始化容量为threshole即可
newCap = oldThr;
//如果进入在这里
//就表明是无参构造,而且是第一次put
else { // zero initial threshold signifies using defaults
//新容量为默认的16
newCap = DEFAULT_INITIAL_CAPACITY;
//扩容阈值为默认容量乘上负载因子
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//现在已经初始化好容量了
//接下来是初始化新的扩容阈值
//这里是针对第一次put的
//在扩容为2倍后,新的扩容阈值newThr也是扩容了两倍,这里不必再进行
if (newThr == 0) {
//让扩容阈值为新的容量乘上复杂因子
float ft = (float)newCap * loadFactor;
//判断扩容阈值是否小于允许最大的底层数组大小
//如果不小于,就让扩容阈值变为Integer最大值,不让下一次扩容进行
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;
//使用for循环来遍历底层的数组
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//遍历找到不为空的位置
if ((e = oldTab[j]) != null) {
//将这个位置设为空,让gc可以回收不要的引用
oldTab[j] = null;
//判断这个位置之前是否产生了冲突
//如果没产生冲突,就代表这个位置只有这个键值对
if (e.next == null)
//不需要重新hash,使用旧的hash值来与上新的底层数组长度
//获得新的索引位置
//让新数组的这个位置存储这个值
newTab[e.hash & (newCap - 1)] = e;
//如果这个位置发生了树化现象
//调用树节点的split方法
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//如果不是树节点,而且后面节点不为null
//代表形成了链表
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;
//索引值不变的结点放在loHead链表上
if ((e.hash & oldCap) == 0) {
//采用的是尾插法
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//索引值需要改变的结点放在hiHead链表上
//采用的是尾插法
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//loHead是索引值不变的链表,所以直接放在原位置上
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//hiHead是索引值需要改变的链表,放在原位置的后面oldCap位置上
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//最后结束
//返回新的数组
return newTab;
}
可以看到,resize方法挺复杂的,主要涉及到三个方面
-
计算新的容量(2倍)
-
计算新的扩容阈值(2倍)
-
让底层数组指向新容量、新扩容阈值的数组
-
新数组复制旧数组里面的内容(如果是树结点就执行split,不冲突的进行重新计算索引值即rehash,根据位置是否变化生成链表)
这里有一个JDK1.7与JDK1.8不同点就是,JDK1.7由于是先进行扩容,然后进行插入新的键值对,对之前旧的元素,是全部遍历然后进行rehash的,而jdk1.8不一样,对于单独一个旧元素会进行rehash,而对于发生哈希冲突产生的链表,里面的结点则会分两种情况,一种是还是放在原位置,另一种情况是要放在后面的位置(原先位置加上旧数组的长度)
这里解释一下为什么
首先,获得索引的方法是哈希值余上数组容量,所以哈希值可以是由数组容量的倍数加上一个低于数组容量的数加起来而成
hashcode = n * capacity + j //公式
现在发生扩容了,新的容量为旧的容量的两倍,就会发生两种情况,这两种情况都是由n倍的旧数组容量产生
hashcode = m * newCap + k //公式
hashcode = m * 2 * oldCap + k = n * oldCap + j
//分两种情况
//当n为偶数的时候,m = n/2,k = j,所以位置是一样的
//当n为奇数的时候,m = n/2是一个小数,不满足
//所以只能让m为n-1的两倍,然后k进行补充
//即m = (n - 1)/2 , k = j+oldCap,
所以新位置可能是与原先的位置一样,也有可能不一样,为原先位置的后oldCap个
关键就在于n是奇数还是偶数
在数组容量为二次幂的前提下,将hashcode化成二进制,同理也是由数组容量的倍数加上一个低于数组容量的数加起来而成,我们就可以不用管低于数组容量的那一部分先,只关注数组容量的倍数那一部分,如果是奇数倍的话,那么这一部分与数组容量进行与运算必定不为0,为数组容量,如果是偶数倍的话,这一部分与数组容量进行与运算一定为0
举个栗子
数组容量为16
16的二进制为10000,那么2 * 16 = 10000+10000 = 10000 ,那么3 * 16 = 10000+10000+10000 = 110000,那么4 * 16 = 1000000,那么5 * 16 = 1010000,可以看到只要是奇数倍,那么化为二进制后,肯定会留有数组容量的二进制,与原来数组容量进行与运算,这后面的一部分一定不会为0
所以就利用这个关系就可以判断n为奇数还是偶数了,所以只需要让hashcode与原来容量进行与运算,判断是否为0,就可以得知n为奇数还是偶数,如果为0,就代表n为偶数,就直接放在原位置,如果不为0,就代表n为奇数,要放在原位置后面的oldCap处
split
接下来我们看看,如果产生哈希冲突形成的结构是红黑树是会进行怎样的处理,也就是对应的split方法,这个方法其实也是用来判断元素位置,只不过这里遍历的对象是红黑树,而且还要考虑退化
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
//同理使用两个链表来记录两种位置的元素(虽然这里使用的是树结点)
//一个是索引位置不发生变化的红黑树
//另一个是索引位置发生变化的红黑树
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
//使用两个int变量来记录两个链表的长度
//如果最后这两个变量小于树化的计数阈值,就要进行红黑树退化
int lc = 0, hc = 0;
//遍历传来的树(底层不仅维护着红黑树也维护着一个链表)
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
//这里同样是判断元素是否需要改变位置,与前面的判断是一致的
//loHead链表存储n为偶数的情况的元素
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
//插入第一个元素
loHead = e;
else
//使用尾插法,形成链表记录
loTail.next = e;
loTail = e;
//记录个数,可能会进行退化
++lc;
}
//hiHead链表存储n为奇数的情况的元素
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
//使用尾插法
hiTail.next = e;
hiTail = e;
//记录个数,可能会进行退化
++hc;
}
}
//下面这一部分就是进行将旧元素存放进新数组了
if (loHead != null) {
//判断不改变元素的红黑树是否需要进行退化
if (lc <= UNTREEIFY_THRESHOLD)
//调用untreeify进行退化
tab[index] = loHead.untreeify(map);
else {
//不需要退化则要进行树化
//将不变索引的元素放在原先的位置
tab[index] = loHead;
//如果另一棵树不为空,代表这棵树只拥有部分元素
//需要重新进行树化
//如果另一棵树为空,代表这课树拥有全部元素
//就不需要重新进行树化,因为本来传进来的就是一棵树
//只不过相当于把树转移进了loHead,然后loHead放进了新数组
if (hiHead != null) // (else is already treeified)
//重新进行树化
loHead.treeify(tab);
}
}
if (hiHead != null) {
//判断改变元素的红黑树是否需要进行退化
if (hc <= UNTREEIFY_THRESHOLD)
//调用untreeify进行退化
tab[index + bit] = hiHead.untreeify(map);
else {
//将索引改变的元素放在改变后的位置
tab[index + bit] = hiHead;
//如果另一棵树不为空,代表这棵树只拥有部分元素
感受:
其实我投简历的时候,都不太敢投递阿里。因为在阿里一面前已经过了字节的三次面试,投阿里的简历一直没被捞,所以以为简历就挂了。
特别感谢一面的面试官捞了我,给了我机会,同时也认可我的努力和态度。对比我的面经和其他大佬的面经,自己真的是运气好。别人8成实力,我可能8成运气。所以对我而言,我要继续加倍努力,弥补自己技术上的不足,以及与科班大佬们基础上的差距。希望自己能继续保持学习的热情,继续努力走下去。
也祝愿各位同学,都能找到自己心动的offer。
分享我在这次面试前所做的准备(刷题复习资料以及一些大佬们的学习笔记和学习路线),都已经整理成了电子文档
= loHead;
//如果另一棵树不为空,代表这棵树只拥有部分元素
//需要重新进行树化
//如果另一棵树为空,代表这课树拥有全部元素
//就不需要重新进行树化,因为本来传进来的就是一棵树
//只不过相当于把树转移进了loHead,然后loHead放进了新数组
if (hiHead != null) // (else is already treeified)
//重新进行树化
loHead.treeify(tab);
}
}
if (hiHead != null) {
//判断改变元素的红黑树是否需要进行退化
if (hc <= UNTREEIFY_THRESHOLD)
//调用untreeify进行退化
tab[index + bit] = hiHead.untreeify(map);
else {
//将索引改变的元素放在改变后的位置
tab[index + bit] = hiHead;
//如果另一棵树不为空,代表这棵树只拥有部分元素
感受:
其实我投简历的时候,都不太敢投递阿里。因为在阿里一面前已经过了字节的三次面试,投阿里的简历一直没被捞,所以以为简历就挂了。
特别感谢一面的面试官捞了我,给了我机会,同时也认可我的努力和态度。对比我的面经和其他大佬的面经,自己真的是运气好。别人8成实力,我可能8成运气。所以对我而言,我要继续加倍努力,弥补自己技术上的不足,以及与科班大佬们基础上的差距。希望自己能继续保持学习的热情,继续努力走下去。
也祝愿各位同学,都能找到自己心动的offer。
分享我在这次面试前所做的准备(刷题复习资料以及一些大佬们的学习笔记和学习路线),都已经整理成了电子文档
[外链图片转存中…(img-Fc55PZ1J-1714396044388)]