HashMap的扩容机制在Java面试经常被问到,下面重点详解HashMap的扩容机制,主要分为如下三步。
1.达到阈值开始扩容
如下图所示:
HashMap的扩容机制是当HashMap中的元素个数超过了负载因子(loadFactor)与初始容量(initialCapacity)的乘积时,就会触发扩容机制。
默认的构造函数指定了扩容因子:0.75, 默认容量是16,如下所示:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 相当于16 static final float DEFAULT_LOAD_FACTOR = 0.75f; public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
元素数量达到了阈值(即负载因子 * 桶的数量)时,会触发扩容操作,也就是说第1次扩容的动作会在元素个数达到12(16*0.75)的时候触发扩容。
为了尽可能地减少扩容操作的次数,通常会将负载因子设置为一个较小的值,例如0.75,以保证哈希表的容量能够满足存储需求,同时又不会造成太多的空间浪费。
2.开始扩容执行
HashMap通过resize()方法进行扩容,容量规则为2的幂次。
如下所示:
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,也就是hashMap中已经有元素了,或者new对象的时候设置了初始容量 if (oldCap > 0) { //如果以前的容量大于限制的最大容量1<<30,则设置临界值为int的最大值2^31-1 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } /** * 如果以前容量的2倍小于限制的最大容量,同时大于或等于默认的容量16,则设置临界值为以前临界值的2 * 倍,因为threshold = loadFactor*capacity,capacity扩大了2倍,loadFactor不变, * threshold自然也扩大2倍。 */ else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } /** * 在HashMap构造器Hash(int initialCapacity, float loadFactor)中有一句代码,this.threshold * = tableSizeFor(initialCapacity), 表示在调用构造器时,默认是将初始容量暂时赋值给了 * threshold临界值,因此此处相当于将上一次的初始容量赋值给了新的容量。什么情况下会执行到这句?当调用 * 了HashMap(int initialCapacity)构造器,还没有添加元素时 */ else if (oldThr > 0) newCap = oldThr; /** * 调用了默认构造器,初始容量没有设置,因此使用默认容量DEFAULT_INITIAL_CAPACITY(16),临界值 * 就是16*0.75 */ else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } //对临界值做判断,确保其不为0,因为在上面第二种情况(oldThr > 0),并没有计算newThr 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 table = newTab; if (oldTab != null) { //遍历将原来table中的数据放到扩容后的新表中来 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; //没有链表Node节点,直接放到新的table中下标为【e.hash & (newCap - 1)】位置即可 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //如果是treeNode节点,则树上的节点放到newTab中 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //如果e后面还有链表节点,则遍历e所在的链表, else { // 保证顺序 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { //记录下一个节点 next = e.next; /** * newTab的容量是以前旧表容量的两倍,因为数组table下标并不是根据循环逐步递增 * 的,而是通过(table.length-1)& hash计算得到,因此扩容后,存放的位置就 * 可能发生变化,那么到底发生怎样的变化呢,就是由下面的算法得到. * * 通过e.hash & oldCap来判断节点位置通过再次hash算法后,是否会发生改变,如 * 果为0表示不会发生改变,如果为1表示会发生改变。到底怎么理解呢,举个例子: * e.hash = 13 二进制:0000 1101 * oldCap = 32 二进制:0001 0000 * &运算: 0 二进制:0000 0000 * 结论:元素位置在扩容后不会发生改变 */ if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } /** * e.hash = 18 二进制:0001 0010 * oldCap = 32 二进制:0001 0000 * &运算: 32 二进制:0001 0000 * 结论:元素位置在扩容后会发生改变,那么如何改变呢? * newCap = 64 二进制:0010 0000 * 通过(newCap-1)&hash * 即0001 1111 & 0001 0010 得0001 0010,32+2 = 34 */ else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; /** * 若(e.hash & oldCap) == 0,下标不变,将原表某个下标的元素放到扩容表同样 * 下标的位置上 */ newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; /** * 若(e.hash & oldCap) != 0,将原表某个下标的元素放到扩容表中 * [下标+增加的扩容量]的位置上 */ newTab[j + oldCap] = hiHead; } } } } } return newTab; }
扩容会将HashMap的容量(即桶的数量)翻倍,扩容的大小是原来的2倍,并重新计算每个元素在新桶中的位置。
3.新建哈希表存储扩容
扩容操作需要创建一个新的哈希表,并将旧哈希表中的元素重新分配到新哈希表的桶中。
重新分配元素时,HashMap会对每个元素的哈希值取模得到一个新的桶位置,并将元素插入到新的桶中。
扩容操作完成后,HashMap将使用新的哈希表来存储元素,并释放旧哈希表的内存空间。
以上就是HashMap扩容机制详解,更多HashMap底层实现原理,请查看:HashMap底层实现原理(图文超详解)。
陈睿mikechen
10年+大厂架构经验,资深技术专家,就职于阿里巴巴、淘宝、百度等一线互联网大厂。
关注「mikechen」公众号,获取更多技术干货!
后台回复【面试】即可获取《史上最全阿里Java面试题总结》,后台回复【架构】,即可获取《阿里架构师进阶专题全部合集》