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接口。
在jdk源码中,共重载了四个构造函数。构造函数主要初始化了三个参数如下:
initialCapacity 此参数表示当前hashmap对象,默认的初始容量capacity,在源码中,java规定此参数必须是2的幂次值,并且默认的初始值为1<<4 aka 16。 loadFactor 负载因子 threshold HashMap中所能容纳的最大键值对数。HashMap的put操作,源码不是很好理解,涉及到很多的位运算,同时还涉及HashMap的扩容机制。这两点我花了一天的时间,才刚刚理解(2020年8月25日)。
简单总结一下吧:
当我第一眼看到这个代码时,才感觉到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阈值,如果超过,则扩容。上文已经两次提到扩容操作。一起分析下源码:
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 将原对象数组中的元素移至扩容后的新数组。那么在创建这两个参数的时候,主要分三种情况进行考虑:
原对象数组已经初始化,并指定了具体数值的初始化参数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; }在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中,取模运算被替换成了位运算,这样高效。
至于为什么这样可以?参考这篇博文:美团的技术总结,思否总结