Java基础——对HashMap的put和resize方法进行理解
程序员文章站
2022-07-09 14:27:45
Java基础——对HashMap的put和resize方法进行理解前言一、Put方法put方法源码put方法总结:二、resize方法resize源码resize方法总结:前言我们可以看到,在面试所有大厂Java岗位的时候,都有对了解HashMap原理的要求,意识到透彻了解HashMap的底层原理迫在眉睫,所以决定再写一篇博文对HashMap进行记忆和分享。一、Put方法开篇我们直接上干货!put方法源码public V put(K key, V value) { return...
Java基础——对HashMap的put和resize方法进行理解
前言
我们可以看到,在面试所有大厂Java岗位的时候,都有对了解HashMap原理的要求,
意识到透彻了解HashMap的底层原理迫在眉睫,所以决定再写一篇博文对HashMap进行记忆和分享。
一、Put方法
开篇我们直接上干货!
put方法源码
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
额,没什么好看的,我们继续深入putVal
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
// 首先判断现在的Hash表是不是为空,如果是空的话进行resize扩容(该方法后面会讲到)
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 通过(n - 1) & hash计算索引
if ((p = tab[i = (n - 1) & hash]) == null)
// 如果索引位置为空,则将数据直接添加入Hash表
tab[i] = newNode(hash, key, value, null);
else {
// 如果不为空,表示该位置已经有值,则进行下面的操作
HashMap.Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 如果该位置的key值和我们要put进去的key值相同,则把对象取出来存在e变量里
e = p;
else if (p instanceof HashMap.TreeNode)
// 这里判断如果当前位置存储的是红黑树结构,则直接进行红黑树的插入操作
e = ((HashMap.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);
// 判断添加后链表元素是否超过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;
p = e;
}
}
//如果存在key与put进入的key相同,则进行元素的替换
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 方法回调
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 判断是否达到扩容临界值,达到则进行resize扩容操作
if (++size > threshold)
resize();
//方法回调
afterNodeInsertion(evict);
return null;
}
看到这么大一片源代码以及注释是不是头都晕了,不着急,我们再进行一波回顾和总结、
put方法总结:
- 首先我们调用HashMap的put方法后,需要对Hash表进行非空判断,空则使用resize方法进行扩容,非空则继续。
- 接着通过hash计算索引位置,索引位置为空则直接进行添加,非空则继续。
- 接着判断该位置的key值和我们要put的key值是否一样,一样则保存在变量里,以便后面进行替换操作,不一样则进行结构判断,判断该位置是红黑树结构还是链表结构,是红黑树结构则直接进行红黑树的结点添加操作,是链表则进行遍历,使用尾插法将结点添加入链表中,添加成功后进行容量判断,如果添加后容量超过既定容量8,则调用方法将链表转化为红黑树结构。
- 最后判断Hash表的容量是否达到了扩容的临界值,如果达到了则进行resize的扩容操作。
通过这四步,我们就完成了HashMap调用put方法进行添加元素的操作。
二、resize方法
还是老样子,上干货!
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) {
// 当当前table容量大于最大值的时候返回当前table
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 把新表的长度设置为旧表长度的两倍,newCap=2*oldCap
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
// 默认初始化容量为 16
newCap = DEFAULT_INITIAL_CAPACITY;
// 默认初始化扩容临界值 16*0.75 = 12
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;
// 创建新的 table
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 如果旧数组不为空,需要将旧table里面的内容,复制到新table里面
if (oldTab != null) {
// 遍历整个oldtable数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
// 取到i之后,里面设置为null 防止多线程环境下循环引用 这个是对jdk1.7的一个改进
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;
// 从表头节点e 开始循环遍历处理冲突的元素
do {
next = e.next;
//结果为 1:那么该元素应该放在新table的新位置
//结果为 0:说明该元素,放在新table的位置与旧table相同 后面会做记录
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
// 把放在新索引(原索引 + oldCap)处的元素 建立新链表
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
// 把放入原索引处的链表 插入到新table中;
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
// 把放入新索引处的链表放 插入到新的table中
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
resize方法总结:
resize()方法是用来扩容的,就是当首次执行put方法,或者当添加put执行完毕后,会检查size是否大于扩容临界值,如果大于临界值,就要执行扩容操作。生成一个新的table数组,这样也就牵涉一个问题-----内容的复制。
- 当旧的数组,只有一个元素,就是判断出它next==null,也就是说没有冲突,那就直接把该元素,该元素放置到新table里面同样索引的位置。
- 如果要复制的节点 是一个红黑树型节点,进行红黑树操作
- 如果要复制的节点下存在冲突,也就是有链表存在。那就从头结点开始遍历,是先通过一个巧妙的运算e.hash & oldCap,这个运算的结果,只有两种 1 和 0 。用来判断该元素,在新table中索引的位置是否发生变化。
结果是:0 直接元素存放在 newtable[ j ]
结果是:1 存放在newtable[ j+oldCap ]
好了,今天对于HashMap的put和resize方法的理解就到这里结束了,如果还有什么问题请在评论区告诉我!
本文地址:https://blog.csdn.net/u013523775/article/details/109953083