HashMap 扩容机制

235 阅读5分钟

今天被问到了常见的三个需要数组扩容的类。分别是StringBuffer/StringBuilder,ArrayList与HashMap。这篇文章就先从源码入手,分析一下HashMap扩容的原理实现。

HashMap的来源

集合类可以分为两大块,分别是Collection与Map。Map下有AbstractMap抽象类实现了Map接口,而HashMap在继承了AbstractMap类的同时实现了Map接口。

相关常量

HashMap有以下四个常量和数据组扩容机制有关。
1.设置初始的数组容量为2的4次方:static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
2.设置最大的容量为2的30次方:static final int MAXIMUM_CAPACITY = 1 << 30;
3.设置默认的负载因子为0.75:static final float DEFAULT_LOAD_FACTOR = 0.75f;
4.设置将list转换为tree的计数阈值,该值必须大于2,而且应该最少为8:static final int TREEIFY_THRESHOLD = 8;

图片.png

put()函数

首先看一下增加元素的入口,put()函数。

图片.png

图片.png

可以看到,将K,V传入后,首先对key调用了hash函数。由于HashMap允许传入null值,因此hash函数中做了对key为null的判断。若其为null就返回0,否则返回key的hashCode()函数的返回值与其无符号右移16位的值的异或值。这里稍微掰开了说。
首先是key.hashCode()。hashCode()是Object类中的一个由native修饰的函数,其主要作用是返回对象的哈希值。随后将这个哈希值与其本身的高16位进行异或运算。这一步的操作是为了让得到的值均匀的分布在\[0,数组长度-1]之间。
随后将运算过的哈希值,key,value一起传入putVal()函数。

putVal()函数

首先看这一段代码。

图片.png

第一个if,如果目前还没有建立数组,那么就会生成一个新的数组,并调用resize()方法设置初始容量。就像上面提到的,DEFAULT\_INITIAL\_CAPACITY设置默认的初始容量为16(需要注意的是,容量只能为2的倍数,这个和HashMap的哈希算法有关,下篇文章会细说)。
随后看第二个if,其中有tab\[i=(n-1)\&hash]。这里的tab就是存值的数组,而(n-1)\&hash是与哈希算法相关的一个计算式,下篇文章会细讲。整体的意思就是计算出传入的值的索引,如果数组在索引的位置处为null,就把该值存入到数组这个索引对应的位置处。
如果该位置已经存在元素,那么就会进入到else当中。

图片.png

在第一个if中,其做了两个判断。首先需要`p.hash == hash`,也就是新传入的哈希值与原来链表头部的哈希值相等。其次需要`(k = p.key) == key || (key != null && key.equals(k))`也就是判断key值相等。同时满足这两个条件,那么就认为当前传入的元素与已经存在的元素是同一个key值。
在else if中,判断如果目前是红黑树的数据结构,那么就按照红黑树的方式进行存储。若都不是,则进入到else中。

图片.png

可以看到,这个else是个死循环。若想跳出这个死循环,则只能有两个方式。第一个,遍历到链表的尾部。

图片.png

首先将新传入的节点放置在链表的尾部,如果此时链表长度大于等于设置的默认值-1,也就是该链表节点个数大于8时,就会将该链表转换为红黑树的结构进行存储。而在调用treeifyBin()函数时,该函数同样会做一次判断。

GKDR\_GVH3I1PYYC1LYQH4@4.png

如果此时整体节点的数量小于设置的最小树化容量值(默认为64),那么就重新调用resize()函数进行扩容处理。这样处理是因为,使用红黑树的本意就是减少由于Map中节点过多而导致hash冲突过大,效率降低的问题,此时节点并未超过默认的阈值,如果直接红黑树化,反而会因为转换为红黑树而浪费性能。同时这里需要注意的是,在resize()函数中,在进行“将原Node数组中的元素拷贝到新的Node数组”这一步骤时,如果遍历到的节点是个红黑树的元素,那么会执行一个split()方法。

图片.png

该方法会将原来的红黑树拆成两个红黑树,而如果拆分后的红黑树节点数少于`UNTREEIFY_THRESHOLD`,则会将红黑树重新转化为链表结构。
扩展的有些多了,重新回到正题。刚才说了第一个跳出死循环的方法就是遍历到链表尾部。那么第二个跳出死循环的方法就是如同第一个if的条件一样,判断出目前传入的新值与链表中某一个已有的值是相同的key值。

图片.png

在进行完这些操作后,将新的值覆盖原先的旧值。随后更新数组中的元素个数,如果超过了容量,就重新进行扩容。大小同样是二倍。