put()方法的源码如下:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
最核心的就是调用了putVal()方法,用来将元素添加到HashMap集合中,但传递的一个参数中调用了一个hash(key)方法,在JDK1.7中的HashMap源码中也有一个hash(key)方法,下面先来看看两个的代码:
1.7版本的hash(key)方法:
1.8版本的hash(key)方法:
其实两个hash(key)方法的原理都是一样的,都是返回经过扰动算法运算的key所对应的hash值。
而在1.7版本中进行了四次扰动运算,即无符号右移了4次,但在1.8版本中只进行了一次扰动运算。
扰动函数是为了让高位也参与运算,降低不同key值发生哈希冲突的概率。
关于hash(key)方法更详细的说明请参考:https://blog.csdn.net/cnds123321/article/details/113745628
知道了hash(key)的结果是什么后,下面才来着重研究putVal()方法。
其源码如下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
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) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
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中
*
* @param hash key所对应的哈希值
* @param key 键值对中的键(key)
* @param value 键值对中的值(value)
* @param onlyIfAbsent 如果存在相同的值,是否替换已有的值,true表示替换,false表示不替换
* @param evict 表是否在创建模式,如果为false,则表是在创建模式
* @return 返回旧值或者null
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
HashMap.Node[] tab;// 临时变量,用来临时存放链表数组
HashMap.Node p;
int n, i;
// 检查链表数组table是否为空,table为null或者table数组的长度为0都表示为空
if ((tab = table) == null || (n = tab.length) == 0)
// 如果为空则初始化,并扩容,然后返回新链表数组的长度,将长度赋值给变量n
// resize()方法就是初始化并扩容,该方法具体请参考:
n = (tab = resize()).length;
// (n-1)&hash这条语句就是JDK1.7中HashMap源码中的indexFor()方法的功能,即得到该对象存放在数组中的具体位置(下标)
// 判断该位置的元素是否为null,即是否存在元素,如果存在则表示已经发生哈希冲突,如果不存在,则添加元素结点
if ((p = tab[i = (n - 1) & hash]) == null)
// 表示不存在元素的情况
// 则新添加一个元素到链表数组的对应下标位置,该结点也是链表的链头
tab[i] = newNode(hash, key, value, null);
else {
// 表示存在元素的情况
// 则发生了哈希冲突,下面的代码则是尝试解决冲突问题
HashMap.Node e;
K k;
// 判断待添加元素的hash值和key值是否同已经存在(冲突)的元素的hash值和key值同时相等
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
// 如果相等,则表示两个元素相互重复了,那么使用变量e来临时存储这个重复元素
e = p;
// 如果不相等,表示没有重复,并且判断结点类型是否是红黑树类型
else if (p instanceof HashMap.TreeNode)
// 那么就将该键值对存储到红黑树中
e = ((HashMap.TreeNode) p).putTreeVal(this, tab, hash, key, value);
// 如果不相等,且结点类型不是红黑树类型,那么就是链表,即采用拉链法解决冲突
else {
// 遍历链表中所有结点,这是一个死循环,需要通过break跳出循环
for (int binCount = 0; ; ++binCount) {
// 如果p的下一个结点为null,则p是链表中的最后一个结点
if ((e = p.next) == null) {
// 则将键值对添加到最后一个结点的后面
p.next = newNode(hash, key, value, null);
// 同时binCount也是一个计数器,统计该链表已经有几个元素了
// TREEIFY_THRESHOLD是常量,表示阈值,默认值为8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 但链表中元素个数超过了阈值,则将链表转换成红黑树
treeifyBin(tab, hash);
// 跳出循环
break;
}
// 判断待添加元素的hash值和key值是否同链表中已有元素的hash值和key值同时相等
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
// 如果相等,则表示已经存在相同的键,跳出循环
break;
// 将下一个节点赋值给当前节点,继续往下遍历链表
p = e;
}
}
// 如果e不为空,则表示已经存在重复的值,即存在hash值和key值同时相等的元素
if (e != null) {
// 保存旧值
V oldValue = e.value;
// 然后替换为新值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 此函数会将链表中最近使用的Node节点放到链表末端,因为未使用的节点下次使用的概率较低
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 记录修改次数
++modCount;
// 如果添加元素后,超过阈值
if (++size > threshold)
// 则对HashMap进行扩容
resize();
// 给LinkedHashMap使用
afterNodeInsertion(evict);
return null;
}
putVal()方法的源码有了注释后就很好理解了,但在该方法中还调用了一些其他方法,这里只简单说明,不详细说明。
关于在代码注释中提到的indexFor()方法可以参考:https://blog.csdn.net/cnds123321/article/details/113741218
该篇写的是1.7版本和1.8版本中HashMap的put()方法添加键值对的解析,特别是有两张流程图能够更好的理解源码。