所有分类
  • 所有分类
  • 未分类

Java-HashMap扩容的原理

简介

说明

本文介绍Java的HashMap是如何扩容的。

重要大小

初始容量最大容量扩容时倍数加载因子底层实现
HashMap2^42^30n * 20.75Map.Entry
HashSet2^42^30n * 20.75HashMap<E,Object>
HashTable11Integer.MAX_VALUE – 8n*2 + 10.75Hashtable.Entry

上表中的“容量”其实就是数组长度。

HashMap中,哈希桶数组table的长度length大小必须为2的n次方(非质数),这是一种非常规的设计,常规的设计是把桶的大小设计为质数。相对来说质数导致冲突的概率要小于非质数,Hashtable初始化桶大小为11,就是桶大小设计为质数的应用(Hashtable扩容后不能保证还是质数)。

HashMap采用这种非常规设计,主要是为了在取模和扩容时做优化。为了减少冲突,HashMap定位哈希桶索引位置时,也加入了高位参与运算的过程。

何时扩容

HashMap是懒加载,构造完HashMap对象后,若没用 put 来插入元素,HashMap不会去初始化或者扩容table,此时table是空的。扩容有如下场景:

  1. 首次调用put方法时,HashMap会发现table为空然后调用resize方法进行初始化。
  2. 非首次调用put方法时,若HashMap发现size(元素个数)大于threshold(阈值)(数组长度乘以加载因子的值),则会调用resize方法进行扩容。
  3. 链表长度大于8 且数组长度小于64 会进行扩容。
    • 链表长度大于8 (且数组长度大于等于64),会转化为红黑树。

数组是无法自动扩容的,所以只能是换一个更大的数组去装填以前的元素和将要添加的新元素。

resize()概述

  1. 判断扩容前的旧数组容量是否已经达到最大(2^30)了
    1. 若达到则修改阈值为Integer的最大值(2^31 – 1),以后就不会扩容了。
    2. 若没达到,则修改数组大小为原来的2倍
  2. 以新数组大小创建新的数组(Node<K, V>[])
  3. 将数据转移到新的数组(Node[])里
    • 不一定所有的节点都要换位置。比如:原数组大小为16,扩容后为32。若原来有hash值为1和17两个数据,他们对16取余都是1,在同一个桶里;扩容后,1对32取余仍然是1,而17对32取余却成了17,需要换个位置。(对应的代码为:if ((e.hash & oldCap) == 0)  若为true,则不需要换位置。
  4. 返回新的Node<K, V>[] 数组

源码

HashMap#resize()

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient Node<K,V>[] table;
int threshold;
final float loadFactor;

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;
                        // 重点1:判断节点在resize之后是否需要改变在数组中的位置
                        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);
                    // 重点2:将某节点中的链表分割重组为两个链表:一个需要改变位置,另一个不需要改变位置
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
5

评论2

请先

  1. 怎么办,从hashmap扩容开始,就记不住了
    简单爱 2024-04-10 0
    • 记住大概流程就行了,还有加载因子是多少。
      自学精灵 2024-04-10 0
显示验证码
没有账号?注册  忘记密码?

社交账号快速登录