前言
HashMap 基于哈希表的 Map 接口实现,其中的值是以 key-value 存储形式存在,即主要用来存放键值对。它的 key、value 都可以为 null,此外,HashMap 中的映射不是有序的。那么本篇文章将从源码的角度来很详细地讲解HashMap中put方法的执行流程
数据结构
在了解其真正流程前我们要先知道HashMap底层使用的数据结构,这个在不同的JDK版本中是不同的。在JDK1.8之前HashMap的底层是由数组+链表组成的。形成一个拉链型的结构。如下图:
为什么要使用这样的结构呢?因为要解决哈希冲突。
什么是哈希冲突?
哈希冲突(Hash Collision)是指在使用哈希表存储数据时,两个或多个不同的键(Key)被哈希函数映射到同一个位置的情况,因为由哈希函数所产生的哈希值是有限的,有可能会出现相同的哈希值。
因此为了解决哈希冲突,采用了这种数组加链表的数据结构,当发生哈希冲突时,会将冲突的值继续放在链表上。
而在JDK1.8之后,HashMap底层数据结构就变为了数组加链表加红黑树。
这样做到主要目的是当表中数据过大导致数组很长,链表长度过长时,查询数据就需要遍历整个链表,这样速度会很慢,因此在JDK1.8中当链表过长时链表就会自动转变为红黑树,这样可以提高查询效率。
put方法解析
下面我们来真正步入正题,我们来看HashMap在执行put方法时底层究竟是如何执行的。
初始化
首先我们进入put方法,可以发现其是调用了putVal方法。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
该方法中有几个参数,首先是再次调用了hash方法,然后是key和value.
我们来看hash方法,其会返回一个整数值,该整数值为key的hashcode值和其无符号右移16的数,两个数做异或操作的结果。这样是为了使得到的哈希值更加分析尽量减少冲突。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
其他的四个参数没什么要说的。
我们直接继续来看putVal方法。
该方法中代码较多,我们一步一步来看。
首先其设置了几个变量,tab和p是一个节点。
再将t现在已有table赋值给tab,并判断其是否为空,这样写可以减少很多代码。如果其为空就证明是第一次使用,并将tab设置为resize()方法的返回值,并让再把长度赋值给n。
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;
我们继续再来看resize方法,这个方法首次使用用来初始化,当不为map不为空时用于扩容(具体如何扩容的后面会说)
首先其设置了一个oldtab变量并将其设置为一个table(第一次使用时table为空),随后判断其是否为空,如果为空就将参数oldCap设置为0,后又设置了一个oldThr阈值,也为0,这几个表示老的。然后由设置了几个新的参数newCap,newThr,同样为0.
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
因为其容量为正常来说设置为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);
}
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
我们可以看到其默认容量为1左移4位,也就是16.因此从此可知hashMap默认容量为16
而新的阈值是默认容量乘以负载因子。负载因子为0.75,那么新的阈值也就是12.
随后按照新的容量设置了一个新的节点格式的数组。并将其作为我们新的数组table设置上,然后判断了老数组是否为空,因此老数组为空,因此我们直接跳过,返回新数组。
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//下面if中的都跳过,下面用来进一步扩容的
//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;
这样我们就拿到了我们新构建的一个数组。
以上的步骤都是在我们当前的数组为空的情况下进行的,因此这一步我们其实在给底层数组进行初始化。
put新值且没有发生哈希冲突
随后我们继续来看purVal下面的代码
首先时第一种情况:我们put新值且没有发生哈希冲突
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
在这一步中将数组中第(n-1与传的hash值做与操作得到的值)个数字的值将其赋给p,如果这个p为空,那么就在此处放置一个新的节点并放置了key,value。
这一步通俗讲就是如果这个位置为空,那么就将要要插入的节点放到此处。
为什么要怎么选择这个位置?
因为我们要通过hash值来快速找到我们要插入的位置,会根据我们二进制hash值得后几位进行插入。
put的key已经存在
第二种情况,put的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) {
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;
}
}
首先设置了一个新的节点e,
如果我们p处已有节点的hash值等于我们传的hash值,并且key也相等,那么直接将p设置给e。很显然这次的情况就是我们给同一个key做了两次put操作。
随后再往下,如果此时的e不为空就将e的值赋给老值,再将e的值设置为我们的传进来的新值,最后返回老值,这样就相当于我们直接把值给覆盖了。
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
因此以上的情况就是我们put了一个已经有了的值,我们用新值覆盖了旧值。
put新的key且发生了哈希冲突
下面继续来看不等,也就是put了一个新的key且发生了哈希冲突的情况
现在这种情况走的就是下面的代码.
代码中首先先开始遍历,当遍历到最后一个节点后的那个空节点时将该节点设置为我们的新节点(附带key,value),相当于插入了一个节点。
当插入完后会进行判断,如果插入后的长度大于最大的长度8时,调用转化为红黑树的方法,即treeifyBin方法。
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;
}
所以我们现在来看一下treeifyBin方法,可以看到进入方法后会先进行一个判断,如果当前底层数组为空或者长度小于MIN_TREEIFY_CAPACITY(此值为64)时会进行扩容。否则会将链表转化红黑树
因此从这里我们可以知道hashmap中链表转化为红黑树的条件
hashmap会在链表长度超过8并且数组长度超过64时将链表转化为红黑树.
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
现在我们再回到resize方法中查看是如何扩容的
下面代码在进行扩容
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;
可以看到扩容时会进行判断,如果现有容量已经大于等于最大容量则不会再继续扩容,会将阈值设为最大值并将已经容量直接返回。(一般达不到这么大超过最大容量)
如果没有达到最大容量则会正常进行扩容,将oldCap左移一位,也就是扩大2倍。
扩大完后,我们再次遍历一边老的数组,对于数组中每一个不为空的值,下一个如果为空,则会将这个空值放入新数组对应的位置(用hash决定)当中。
如果是红黑树会用split方法将其拆开。
再其次,会将这整条链表拆为低链和高链。
那么是如何判断哪些节点在高链,哪些节点在低链呢?
我们是根据原节点的hash值和老容量做与操作,如果为0是低链,如果为1为高链。
然后低链留在当前位置,高链去我们当前容量加上老容量的对应位置。这样就完成了扩容。
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;
如果不需进行扩容,就会正常将链表转化为红黑树
也就是下面代码,下面代码比较简单就不再多做介绍了。
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
还有一种情况,如果是在增加链表时的出现了已经存在key值,也同样会进行覆盖,直接return old节点,和上面时一样。
最后提一点,每次添加完节点和都会进行容量++(无论时链表拉长还是红黑树中添加节点),覆盖除外。
至此,本篇文章的全部内容正式结束,我感觉我在这里面对于源码的解读已经很详细,解释了hsahmap在put时的过程以及会发生的所有情况。
希望我的总结会对你产生一定的帮助,帮助你更好地理解hashmap.