先看put方法保存数据,内部调用putVal()
方法保存到map,onlyIfAbsent参数的意思是传true时表示如果key对应的value存在就不会保存,evict暂不考虑,这是LinkedHashMap中实现的。找到节点的过程是:
- 数组不存在时创建数组
- 根据节点的哈希值计算出来的索引处没有节点时,新建一个
newNode
- 判断链表的第一个是否为要更新的节点
- 否则遍历链表,如果遍历到链表尾部则新增
newNode
;如果发现链表深度大于阈值TREEIFY_THRESHOLD(默认8)时,链表转数,加速查询;如果在链表中发现要更新的节点则返回。
找到节点后更新数据即可。
java
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K, V>[] tab;
Node<K, V> p;
int n, i;
//在第一次调用put方法时为数组分配内存
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//数据保存在数组的i = (n - 1) & hash位置,i表示数组的索引
//如果此处先前没有数据,直接创建节点把数据保存在这里
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K, V> e;
K k;
//如果数组i处的链表中第一个节点的key与保存的key相同,则在后面直接替换value
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) {
//(e = p.next) == null表示遍历到链表中最后一个节点,如果没有发现哪个节点的key与要保存的key相同,链表尾添加的新的Node ,作为链表中最后一个节点
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//当链表的深度大于TREEIFY_THRESHOLD(8)时,可以链表转树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果找到一个节点的key与要保存的key相同,则在后面替换value
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break;
//移动到下个节点
p = e;
}
}
//如果在保存数据到map过程发现已有节点的key与要保存的key相同,则替换已有节点的value,返回之前的value
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;
}
再看下hashmap怎么扩容的,首先计算容量,默认为16(0x1111),这样与运算计算出来的索引就是由节点的hash值决定,引起哈希碰撞的概率就很小。扩容时右移一位即扩大两倍,仍然为2的整数次幂。然后将原来数据拷贝到新数组,为了让数据更加均匀的分布,可以让原来的key的hash值与数组容量与运算,创建两条0和1的链条,分别保存在原理数组对应的索引处和新数组中"原索引+oldCap"处。首先计算扩容后的数组容量为原始数组翻倍newCap = oldCap << 1
,然后将原数组中数据移动到新数组,过程如下:
- 不存在链表,直接移动到新数组的
e.hash & (newCap - 1)
位置处 - 存在链表,如果
if ((e.hash & oldCap) == 0)
,则移动到跟原数组位置相同的地方,否则移动到"原索引+oldCap"处。
现在考虑下为什么e.hash & oldCap
就能判断节点移动后在新数组中的位置,首先原始索引为:oldIndex = hash & (oldCap - 1),新索引本来应该是newIndex = hash & (newCap - 1),但是不要忘了newCap = oldCap << 1
,即计算新的索引时相对原索引只要考虑最高位的值,而最高位的值刚好为oldCap,进而能够减少位运算的位数提升效率。比如,oldCap=16时,oldIndex = hash & (oldCap - 1)为oldIndex = hash & 0x1111,而newCap=32,newIndex = hash & & 0x11111,跟原来的与运算相比结果由最高位0x10000决定的,如果hash的最高位是1就发生变化,结果为原始索引的二进制的oldCap中1的位置处置1,如果oldCap为0x10000,则将计算结果的第五位置1,这样只用考虑这一位的二进制结果即可。
java
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 1.老表的容量不为0,即老表不为空
if (oldCap > 0) {
// 1.1 判断老表的容量是否超过最大容量值:如果超过则将阈值设置为Integer.MAX_VALUE,并直接返回老表,
// 此时oldCap * 2比Integer.MAX_VALUE大,因此无法进行重新分布,只是单纯的将阈值扩容到最大
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 1.2 将newCap赋值为oldCap的2倍,如果newCap<最大容量并且oldCap>=16, 则将新阈值设置为原来的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 2.如果老表的容量为0, 老表的阈值大于0, 是因为初始容量被放入阈值,则将新表的容量设置为老表的阈值
else if (oldThr > 0)
newCap = oldThr;
else {
// 3.老表的容量为0, 老表的阈值为0,这种情况是没有传初始容量的new方法创建的空表,将阈值和容量设置为默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 4.如果新表的阈值为空, 则通过新的容量*负载因子获得阈值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 5.将当前阈值设置为刚计算出来的新的阈值,定义新表,容量为刚计算出来的新容量,将table设置为新定义的表。
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 6.如果老表不为空,则需遍历所有节点,将节点赋值给新表
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) { // 将索引值为j的老表头节点赋值给e
oldTab[j] = null; // 将老表的节点设置为空, 以便垃圾收集器回收空间
// 7.如果e.next为空, 则代表老表的该位置只有1个节点,计算新表的索引位置, 直接将该节点放在该位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 8.如果是红黑树节点,则进行红黑树的重hash分布(跟链表的hash分布基本相同)
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 9.如果是普通的链表节点,则进行普通的重hash分布
Node<K,V> loHead = null, loTail = null; // 存储索引位置为:"原索引位置"的节点
Node<K,V> hiHead = null, hiTail = null; // 存储索引位置为:"原索引位置+oldCap"的节点
Node<K,V> next;
do {
next = e.next;
//创建两条链表,分别是0和1
// 9.1 如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
if ((e.hash & oldCap) == 0) {
if (loTail == null) // 如果loTail为空, 代表该节点为第一个节点
loHead = e; // 则将loHead赋值为第一个节点
else
loTail.next = e; // 否则将节点添加在loTail后面
loTail = e; // 并将loTail赋值为新增的节点
}
// 9.2 如果e的hash值与老表的容量进行与运算为非0,则扩容后的索引位置为:老表的索引位置+oldCap
else {
if (hiTail == null) // 如果hiTail为空, 代表该节点为第一个节点
hiHead = e; // 则将hiHead赋值为第一个节点
else
hiTail.next = e; // 否则将节点添加在hiTail后面
hiTail = e; // 并将hiTail赋值为新增的节点
}
} while ((e = next) != null);
// 10.如果loTail不为空(说明老表的数据有分布到新表上"原索引位置"的节点),则将最后一个节点
// 的next设为空,并将新表上索引位置为"原索引位置"的节点设置为对应的头节点
//0的链表在数组原来位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 11.如果hiTail不为空(说明老表的数据有分布到新表上"原索引+oldCap位置"的节点),则将最后
// 一个节点的next设为空,并将新表上索引位置为"原索引+oldCap"的节点设置为对应的头节点
//1的链条在数组扩容后的位置
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 12.返回新表
return newTab;
}
总结下,HashMap保存数据会先计算索引,再生成链表,扩容时会将链表移动新数组中。有不对的地方请大神指出,欢迎大家一起讨论交流,共同进步,更多请关注微信公众号 葡萄开源