HashMap部分源码集解
共 7374字,需浏览 15分钟
·
2023-06-05 01:45
本文将主要分析HashMap的重点方法,包括put(), get(), resize()。并对其重点代码进行解释说明。
# 阅读本文需具备基础的知识:
1. 了解Java基础知识;
2. 对HashMap有一定的了解并懂得HashMap中一些基本属性的作用;
## put 方法集解
public V put(K key, V value) {
//计算key的hash值
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node
[] tab; Node p; int n, i; // Node数组非空判断,为空则调用resize方法进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 通过(n - 1) & hash 的方式计算出key的位置,再对当前位置的元素进行非空判断,为null则直接插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node
e; K k; //判断key是否存在,存在则下面逻辑选择性覆盖旧值
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断是否为红黑树结构,是的话以红黑树的方式插入
else if (p instanceof TreeNode)
e = ((TreeNode
)p).putTreeVal(this, tab, hash, key, value); else {
for (int binCount = 0; ; ++binCount) {
// 链表的下一节点为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;
p = e;
}
}
// 如果key已存在,则根据onlyIfAbsent选择性用新值替换旧值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// HashMap线程安全的保障
++modCount;
// 当Node数组的长度大于扩容阈值时则进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
## put重点代码发明
1. 为什么用(n - 1) & hash 的方式计算元素下标?
原因:(n - 1) & hash == hash % n, 并且(n - 1) & hash 的效率要高。(n为2的次幂)
解释:
当n == 8 == 1000, hash == 59 == 111011时。
(n - 1) & hash == 11 并且 111011 == 11 == 3,
59 % 8 == 3
根据位运算规则可知,二进制数中凡是低三位为0的数都可以被100整除,低三位的值即为余数;
利用位与运算,将高位置0,低三位置为原值,结果低三位的原值即为余数。
所以当n为2的次幂时,(n - 1) & hash 即为 hash % n的值
2. putVal方法中onlyIfAbsent的意义及作用
onlyIfAbsent: - 如果存在新key和旧key相同,是否用新值覆盖旧值; - 如果为true,则新值不覆盖旧值。false则进行覆盖。
3. modCount的作用
记录集合被修改的次数。 当操作HashMap时,发现modCount发生改变时进入快速失败,抛出 ConcurrentModificati onException
## resize 方法集解
final Node
[] resize() { Node
[] oldTab = table; // 获取原哈希表容量;若原哈希表为null,则容量为0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 获取原哈希表扩容门槛
int oldThr = threshold;
// 初始化新哈希表容量和新哈希表门槛为0
int newCap, newThr = 0;
// 如果原容量大于0,计算扩容后的容量及新的负载扩容门槛
if (oldCap > 0) {
//判断原容量是否大于等于HashMap允许的容量最大值 - 2的30次幂
if (oldCap >= MAXIMUM_CAPACITY) {
// 如果原容量大于等于允许的最大容量,则把HashMap的扩容门槛设置为Integer允许的最大值
threshold = Integer.MAX_VALUE;
//不再扩容,直接返回
return oldTab;
}
// 把原Node数组容量扩大为原来的2倍作为新Node数组的容量。
// 如果新Node数组的容量小于HashMap所允许的容量的最大值: 2的30次幂,并且原Node数组容量大于等于默认的初始化Node数组容量: 2的4次幂 -> 16,则计算新的扩容门槛
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
// 新Node数组的扩容门槛为原Node数组的扩容门槛的2倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
// 如果原Node数组容量小于等于0并且原Node数组扩容门槛大于0,新Node数组容量为原Node数组扩容门槛大小
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 如果原Node数组容量小于等于0并且原Node数组的扩容门槛小于等于0,则初始化新Node数组容量为默认初始化容量:1 << 4;
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新的扩容门槛等于0,则重新计算新的扩容门槛
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
"rawtypes","unchecked"}) ({
//初始化新NodeNode数组
Node
[] newTab = (Node [])new Node[newCap]; table = newTab;
//旧Node数组不为null,则进行扩容
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node
e; if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
// 计算新Map的Node数组元素下标。如果当前Node节点没有后续节点,则直接放入新位置
newTab[e.hash & (newCap - 1)] = e;
// 如果当前Node是红黑树,则对树进行处理
else if (e instanceof TreeNode)
((TreeNode
)e).split(this, newTab, j, oldCap); // 处理链表
else { // preserve order
Node
loHead = null, loTail = null; Node
hiHead = null, hiTail = null; Node
next; do {
next = e.next;
// (e.hash & oldCap) == 0 元素放在低位区,否则放在高位区
// 低位区:原Node长度的位置;高位区:原Node长度到扩容后Node长度的位置
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);
// 将低位区的尾结点指向null,并把低位区的头结点放入新Node数组的合适位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 将高位区的头结点放到新Node数组合适的位置
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
## resize重点代码发明
-
为什么要进行扩容?
减少hash碰撞,使HashMap中的结构更简单。
2. 为什么要以2的倍数作为扩容的策略?
保证HashMap的容量都是2的次幂,便于key的位置计算和扩容的容量计算。
## get方法集解
public V get(Object key) {
Node
e; //获取key的hash值
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node
getNode(int hash, Object key) { Node
[] tab; Node first, e; int n; K k; //Node数组不为null并且其长度大于0并且key位置部位null,则获取当前key位置的Node,否则返回null
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
//当前位置的key和入参key相等,则返回当前Node
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 如果首Node节点为空,直接返回null。否则去链表或红黑树中取值
if ((e = first.next) != null) {
//判断是否是红黑树,是的话在树中取值
if (first instanceof TreeNode)
return ((TreeNode
)first).getTreeNode(hash, key); do {
//在链表中取值,如果key值相等,则取链表中的当前Node节点
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//返回null
return null;
}
疑问:
现状:在resize方法中,代码 hiTail.next = null; 表示将高位区的尾结点的next指向null。
问:"这个操作不能将高位区和低位区的Node节点通过指针相连,那么遍历链表时如何遍历到低位区的数据?为什么不将高位区尾节点的next指向低位区的头指针?"
注:
集解 - 引自《本草纲目》,意为收集业内众家释义;
发明 - 引自《本草纲目》,意为笔者独立的见解,有别于他。