阅读本专栏需要了解HashMap底层数组+链表+红黑树的数据结构。
最近没大有时间暂时没空组织过多的文字描述和图例,后期我会补上,先直接上源码吧。
putVal
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判断table是否为空
if ((tab = table) == null || (n = tab.length) == 0)
//为空则调用resize方法扩容,并将table长度赋给n
n = (tab = resize()).length;
//检查当前根据hash值计算的当前元素位置是否为空
//计算数组下标的的逻辑(n - 1) & hash,实际上是hash对(n-1)的求余
//因为n是2的次幂(HashMap的初始化和扩容算法决定),所以n的2进制只有最高位是1
// 假设hash值为 478,569,转换成2进制 0000 0000 0000 0111 0100 1101 0110 1001
// 假设n=16,则n的2进制数为 0000 0000 0000 0000 0000 0000 0001 0000
// n-1的2进制数为 0000 0000 0000 0000 0000 0000 0000 1111
// 从二进制可以看出,任何数对2的次幂求余,实际上对于被除数从2的次幂为1的那位开始往上的高位都是可以忽略的,不管这些高位是0还是1,都不影响求余结果
// 而且可以的出,求余的结果肯定就是除了这些高位之外的剩下的地位
// 而对n-1进行与操作,相当于把n-1的2进制数作为一个掩码,进行与运算后,只保留了被除数地位的数据。
// 因此说,对n(也就是2的次幂)求余等同于,对(n-1)做与操作。
if ((p = tab[i = (n - 1) & hash]) == null)
//为空直接创建一个节点放到该位置
tab[i] = newNode(hash, key, value, null);
else {
//不为空的情况
Node<K,V> e; K k;
//先判断当前元素是否是是不是要查找的key
//两个条件都要满足,一个是哈希值必须相同,另一个是Key必须相等(也可以重写key的equals方法,自定义相等的算法)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//找到后,取出当前元素p赋值给e
e = p;
else if (p instanceof TreeNode)
//如果p是红黑树节点,则调用红黑树的put逻辑
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//else的情况说明此处为链表结构,此时通过for循环遍历整个链表,并计数
for (int binCount = 0; ; ++binCount) {
//找到链表的最后,人没有找到
if ((e = p.next) == null) {
//则创建一个节点,挂在链表的最后
p.next = newNode(hash, key, value, null);
//计数的目的在这,如果链表长度>=树化的临界点(TREEIFY_THRESHOLD,值为8)了,
//则调用树化的方法(并不是一定会树化,还要看数组长度是否超过了64,具体逻辑在treeifyBin防范中)
//TREEIFY_THRESHOLD得值为8,为什么是8?这是统计方法计算出来的,
//树化是需要消耗性能的,且树化后,对于该节点上数据的增删都要更负责,但查询效率很高。
//数据量少了,使用链表比较划算,数量超过8了,使用红黑树才有价值
//TREEIFY_THRESHOLD - 1,为什么要-1,因为上面的if已经判断过头结点了,此处是在第二个节点开始遍历的
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果找到了某个相同的key,判断逻辑同上面的第一个if判断
//则跳出循环,此时e即为找到的节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//两个if条件都不满则,则继续循环看下一个节点
p = e;
}
}
//e!=null说明找到了,即存在该key
if (e != null) { // existing mapping for key
//取出该节点的value
V oldValue = e.value;
//如果onlyIfAbsent为false,或者oldValue为null,则直接赋值为新value
//此时要注意,HashMap虽然支持value为null,但是如果传入的onlyIfAbsent为true
//null还是会被替换,这样保证了外层判断返回值为null即为成功的有效性。
//不管这个null是oldValue还是在程序最后直接return的null
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//空方法,供HashMap的子类实现,例如LinkedHashMap
afterNodeAccess(e);
//此处直接返回,因为节点个数无变化,不用执行线面的modCount自增,size自增和resize等方法。
return oldValue;
}
}
//modCount自增
//modCount是迭代器遍历用的,线程不安全的集合都有这个属性
//目的是在迭代器遍历集合期间,如果有其他线程更改了节点数量,则modCount的值会发生改变,
//此时迭代过程中会抛出异常ConcurrentModificationException,这就是所谓fail-fast策略。
//注意到 modCount 不是 volatile,原因是这个变量会被频繁改动,使用volatile的代价会很高。
//而且fail-fast策略不是用来尝试修复这个异常的,而是抛出异常告诉用户,不应这样使用。
++modCount;
//size自增,且判断是否超过扩容阈值
if (++size > threshold)
//超过了,则调用扩容方法
resize();
//空方法,供HashMap的子类实现,例如LinkedHashMap
afterNodeInsertion(evict);
return null;
}
//hash算法
static final int hash(Object key) {
int h;
//不是简单的返回hashCode,而是将计算出来的hashCode,高16位和第16位做异或运算。
//目的是减少哈希值的低位冲突,让高位参与进来,最终计算的哈希值体现高位的特征。
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}