本文用于探索和学习HashMap源码,对HashMap的源码进行解读。
主要学习HashMap的插入和扩容思想。
HashMap在jdk1.8后对其内部数据结构进行了优化,从以前的 数组+链表 的结构改为 数组+链表+红黑树 的结构。
在未发生哈希冲突时仅使用数组进行存储,但发生哈希冲突时,若key值不相同,则插入到冲突数组节点(HashMap的数组中存储数据为Node的节点,属于链表结构)的链表中,若链表长度大于8且数组的长度不小于64的时候通过 treeifyBin() 转化为红黑树提高查询效率。
先从存储方法put()进行解读:
public V put(K key, V value) { //调用了putVal()进行存储 return putVal(hash(key), key, value, false, true); }
HashMap的存储方法put()是通过调用putVal()方法进行存储。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
其中一共传入了5个参数:
hash:使用hash方法计算key的哈希值
key:传入的key值
value:传入的value值
onlyIfAbsent:意义为当key值相同时,是否覆盖原有的key值。为true,不覆盖原有的key值;为false,key值相同时覆盖原有的key值。(默认为false)
evict:是否为创建模式,此处默认为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; 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);
当我们调用putVal()时:
第一个if()先判断创建的HashMap对象是否初始化,若没有初始化则调用resize()方法(resize方法 后文会进行解读)进行初始化扩容。
第二个if()语句中,使用HashMap的长度与hash进行 &操作(HashMap插入的位置是无序的,仅根据hash值与长度n的&操作决定),判断该位置是否存在元素,若不存在则直接插入Node节点,若存在则说明发生了哈希冲突。(哈希冲突:不同的key进行哈希运算后得到相同的哈希值,代表发生了哈希冲突)
else { Node<K,V> 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<K,V>)p).putTreeVal(this, tab, hash, key, value); //第三部分 else { for (int binCount = 0; ; ++binCount) { //第三部分A if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //第三部分B if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //第三部分C if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } }
存储方法put()的主要内容在于发生哈希冲突后的逻辑处理。
第一部分:判断发生哈希冲突的节点与插入节点的hash值、key值是否相同,若相同则插入节点覆盖原节点。
第二部分:判断发生哈希冲突的节点属于红黑树结构,如果为红黑树则直接插入到红黑树中
第三部分:当不属于前两种情况时,则需对该节点的链表进行操作,对当前节点的链表进行遍历。
第三部分A:判断当前节点链表中是否还有后续节点,若没有则插入到链表尾部。
第三部分B:判断链表长度是否 >= 8(因为binCount从0开始,所以TREEIFY_THRESHOLD需要减1),若大于等于8则调用treeifyBin()将链表转化为红黑树的存储结构。
第三部分C:但对当前节点的链表进行遍历时,发现其中节点的hash值、key值与插入节点相同,直接break结束遍历。
if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; }
当 e != null 时,说明插入key值成功了,若 onlyIfAbsent 为false说明采用新值覆盖旧值的原则,或旧值为null时,新值覆盖旧值。afterNodeAccess()用于LinkedHashMap。
++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null;
modCount记录HashMap的修改次数。
因为插入了数据,所以size需要加1,此处if判断HashMap是否需要扩容,需要扩容则调用resize()扩容方法。
afterNodeInsertion()也是用于LinkedHashMap中。