HashMap源码分析

tech2023-10-22  93

HashMap源码分析

1、简介

HashMap是非同步的,即线程不安全的。HashMap中的key和value都可以为null。

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {

HashMap继承自AbstractMap,并且实现了Map、Cloneable、Serializable接口。

2、构造函数

public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }

在jdk源码中,共重载了四个构造函数。构造函数主要初始化了三个参数如下:

initialCapacity 此参数表示当前hashmap对象,默认的初始容量capacity,在源码中,java规定此参数必须是2的幂次值,并且默认的初始值为1<<4 aka 16。 loadFactor 负载因子 threshold HashMap中所能容纳的最大键值对数。

3、HashMap的put操作

HashMap的put操作,源码不是很好理解,涉及到很多的位运算,同时还涉及HashMap的扩容机制。这两点我花了一天的时间,才刚刚理解(2020年8月25日)。

简单总结一下吧:

3.1、Put方法源码

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; 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<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) { 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; }

当我第一眼看到这个代码时,才感觉到java8的代码和java7之间的差异。

逐行分析:

当put key-value键值对的时候,将会调用put方法,进而调用putVal方法。分析putVal方法

int hash :档期需要put的key的hash值K key :插入的KV value : 插入的V

接下来:

Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; 创建Node数组tab,Node变量pif语句中,将用来判断当前hashMap是否被初始化,如果hashmap中的table==null,说明其没有被初始化,此时调用resize()扩容函数进行初始化操作。此处并没有写错,resize()函数对于空hashmap,将进行初始化,后文会有详细介绍。也就是说,此处用来处理没有初始化的情况 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); 此处是第二个if语句,当table不为空时,第一个if语句不满足,则执行此条if语句。判断当前index下存储的元素是否为空,如果为空,则直接插入即可。(n - 1) & hash 此条语句是计算index索引的方式,后文回有讲解。 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) { 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语句均不满足,说明table已经初始化,并且计算得到的index上存在元素,需要做判断,故转到else语句。

if (p.hash == hash &&((k = p.key) == key || (key != null &&key.equals(k)))) e = p; 此条语句表示,当前index上面节点key存在,则此时直接覆盖value即可。 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)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; } 此时java判断之后,将作为链表插入。JDK1.8中规定,如果HashMap上面,链表长度大于8则,转为红黑树结构,提高效率。所以for循环就是在检测,当指针在当前链表上走到末尾时,所经过的元素到底有没有8个,要是有,则交由红黑树处理。如果不超过,则在查找过程中,发现key相同的元素,对其覆盖value。 if (++size > threshold) resize(); 当插入操作完成后,HashMap将自检,其内部的键值对数目是否超过了threshold阈值,如果超过,则扩容。

3.2、Resize() 扩容机制

上文已经两次提到扩容操作。一起分析下源码:

final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }

当我看到这些代码的时候,我已经吐血了

其实归根结底,扩容机制只是更换更大的"桶"来替换原来的小桶。那么什么时候扩容呢?

JDK1.8中规定:

当HashMap中的键值对数量超过HashMap的阈值threshold,立即扩容。

扩容机制实际上做了哪些事呢?

根据原HashMap中的对象数组,创建新的扩容数组的两个参数 CapacityThreshold 将原对象数组中的元素移至扩容后的新数组。
第一件事:
Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr;

那么在创建这两个参数的时候,主要分三种情况进行考虑:

原对象数组已经初始化,并指定了具体数值的初始化参数capacity

if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } 可以看到,如果原对象数组的capacity已经大于默认的最大容量,则直接将阈值设置为默认最大阈值,不在扩容。如果是正常的capacity,则直接将原对象数组的阈值,扩大两倍,源码用的是移位操作。效率更高

old对象数组只初始化了阈值,没有初始化capacity。

else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; 则直接将old对象数组的阈值赋给newCap。

如果old对象数组两个参数都没有初始化

else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } 使用JDK中默认的原始参数进行初始化。

最后,给newThr按照JDK1.8中的计算公式赋值。

if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr;

第一件事完毕。

第二件事:

当第一件事完成后,我们已经有了大的桶。这时只需要将小桶中的元素按照索引的方式放入大桶内即可。

table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { //删除旧对象数组的引用 oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab;

遍历old对象数组

for (int j = 0; j < oldCap; ++j) if (e.next == null) newTab[e.hash & (newCap - 1)] = e; 如果当前index上面只是一个元素节点,而不是链表,则直接根据计算得到的index地址,写入new对象数组,也就是大桶。 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 如果java判断当前节点处在红黑树链上,则交由红黑树算法插入,不做介绍,红黑树太他妈难。 else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } }

java判断当前元素节点并没有处于红黑树链上,而是正常的链表。则需要做进一步的操作

遍历当前这条链表

do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null);

此处包含两种计算方式

(e.hash & oldCap) ==0 按原索引插入(e.hash & oldCap) != 0 按原索引+oldCap插入后面会详细的讲解

插入操作。

if (loTail == null) loHead = e; else loTail.next = e; loTail = e; if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e;

将两个新链表链接起来

if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; }

4、HashMap中桶数组索引地址的计算方式

在HashMap中无论是增删改查,第一步都是要确定操作元素的具体地址,那么如何确定地址也就是索引index呢,下面详细展开:

源码:

static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } static int indexFor(int h, int length) { //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的 return h & (length-1); // 取模运算 }

以上便是桶数组索引地址的计算方式:

求解对象的hashcode值,hashcode()方法是Objects的方法,所以java中的所有对象都具备此方法。高位参与运算取模运算

一一分析:

hashcode()方法没什么可说的了,有时间可以看一下如何实现的。

高位参与运算,其实也很简单,首先在已经得到的hash值的基础上,我们对其右移16位,那么对于32位的int来说,右移之后,高16位对应低16位。然后取亦或操作。如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-di4theld-1599122024926)(C:\Users\30914\AppData\Roaming\Typora\typora-user-images\image-20200825161411166.png)]

最后取模运算,但是在JDK1.8中,取模运算被替换成了位运算,这样高效。

至于为什么这样可以?参考这篇博文:美团的技术总结,思否总结 ​

最新回复(0)