HashMap的put过程
1、计算hash,调用putVal
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
首先会计算出key的hash数值,然后调用putVal函数。
2、putVal函数
这个函数我按照对应的if-else,把它分为如下几个部分:
- 如果表空:
(tab = table) == null || (n = tab.length) == 0
,这个时候当然是进行一些表的扩大,用resize()方法。 - 如果我们把我们待插入的键值对(这里将这个键值对用<hash,key,value>表示)的位置用散列函数散列到表中,并且发现这个表的该位置(p)是null的,说明之前没有出现过这个key,所以直接创建一个新节点,放入这个位置(i)。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
这个地方稍微举一个例子,比如我们当前tab是一个长度n为4的表,其中0和1的位置有数据,2和3的位置是空,当前插入了一个key的hash为2,v为x的键值对,那么利用上面的散列计算方式((n-1)&hash)我们算的其应该插入的位置为i=2,而我们发现(p=tab[2])==null,所以可以直接创建一个新的节点存到表中。其实这里之所以用&,是因为我们在hashmap中长度规定为2^n,所以&相当于取余。
-
如果这个位置有元素,那么我们需要做的工作其实就是找到这个元素应该插入的位置。因为hashmap解决hash冲突的时候采用的是拉链法,所以这个元素要么是附在这个拉链的末端,要么在拉链中找到了一个同样的元素,需要更新对应v。所以下面又有如下步骤。
3.1. 首先看这个p的key是否和我们待插入的键值对是相同的,这里的判断方法是:
p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))
其实就是先看hash值,如果hash值同的话,再看具体数值。这样可以减少equals计算次数。如果我们计算发现p刚好和待插入的元素一样,那么就先用e
记录这个位置。
3.2. 因为p其实相当于拉链的头,其实可以看作是一个链表的表头,我们刚才就是判断了一下链表的表头是否和插入的同,如果不同的话,我们就要去链表表里面找了,在链表里面找则有两种情况,一种是找到了,一种是没有找到。不过源码中在进行链表搜索前还有如下一步:
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
这个其实是jdk1.8的一个优化,如果我们链表过长,那么顺序搜索会影响性能,所以会转换成红黑树。所以这里先看p是不是TreeNode,如果是,则用对应方法插入。
3.3. 如果是正常链表,那么就开始了一个for循环,这个for循环里主要是遍历这个链表,从for里面第一句if ((e = p.next)
和最后一句p =e
可以看出主要就是一个遍历。而里面涉及了两个判断,第一个是判断了if ((e = p.next) == null)
,如果是为null,那么代表链表到达了尾部,那么我们做的就是在链表尾部插入一个新节点即可。另外一个判断是
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
这个主要判断遍历的这个节点是否和我们待插入的数据相同,如果相同,那么说明里面有重复的节点,它做的操作就是保存这个节点即可(也就是对应的e
),这个e和3.1里面的那个e刚好是统一起来,都是代表如果找到了一个完全一样的节点。
再3.2和3.3遍历的过程中还有一个binCount,这个主要记录链表长度,如果长度超过了TREEIFY_THRESHOLD - 1,那么就将链表转为红黑树。
最后就再判断一下e是否为null,如果不是null,那么表示里面找到了一个完全和待插入key一样的元素,那么我们只需要更新v即可。
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}