HashMap如何计算初始化容量,最大容量是多少

摘要:结合HashMap源码,介绍HashMap如何确定初始化容量,其最大容量是多少。

  更多关于HashMap的知识点,请戳《HashMap知识点梳理、常见面试题和源码分析》。

  本文基于Java 17进行分析。

  什么是HashMap的容量?容量就是HashMap中的数组大小或者桶的数量,是由 capacity 这个参数确定的。初始容量只是哈希表在创建时的容量。

  大家都知道HashMap是采用的懒加载机制,也就是说在执行new HashMap()的时候,构造方法并没有在构造出HashMap实例的同时也把HashMap实例里所需的数组给初始化。那么,什么时候才去初始化里面的数组呢?答案是在第一次用到数组的时候才会去初始化它,就是在向HashMap里面添加元素的时候。而初始化数组时,它的容量是怎么确定的呢?有两种情况:
第一种是调无参构造函数初始化实例。此时默认的数组初始化长度就是16,在后续添加元素时,进行数组初始化。

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

  第二种是调用带数组容量参数的构造函数。

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

或者


/**
 * 如果构造函数传入的值大于该数,则替换成该数。
 * The maximum capacity, used if a higher value is implicitly specified
 * by either of the constructors with arguments.
 * MUST be a power of two <= 1<<30.
 */
static final int MAXIMUM_CAPACITY = 1 << 30; // ①
    /**
     * The next size value at which to resize (capacity * load factor).
     * 数组扩容的阈值
     * @serial
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    int threshold;

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY) // ② 判断是否超过最大容量
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

  显而易见,上面那个构造方法执行的时候调用的就是下一个构造方法 this(initialCapacity, DEFAULT_LOAD_FACTOR)。当你调用带参构造器初始化一个指定数组容量的HashMap时,构造器会根据输入的参数提前计算出数组实际的长度,这个值也是在首次添加元素时起作用。计算的逻辑在函数 tableSizeFor(int cap) 中,源码如下:

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; // ③ 判断是否超过最大容量
}

  它对入参cap减一之后使用了无符号右移,然后进行或运算,将n-1得到的值转成2进制之后,从1的最高位开始将低位全部转化为1,再加1之后就可以得到一个2^n的数。

HashMap的最大容量是2^30

  HashMap的最大容量是多少?容量默认是16,也可以构造时传入,最大值是1<<30,即2^30,这个在源码①的注释中已经明确说明。首先必须理解操作符 <<,它是左移操作符,表示对二进制进行左移。通常情况下,1 << x 等于 2^x

  上一节中②和③所标记的代码表明,如果要存的元素数目大于 MAXIMUM_CAPACITY,HashMap方法还把数组大小capacity强制设置成 MAXIMUM_CAPACITY
  综上所述,HashMap限制数组大小最大值有两个地方,其一就是初始化时调用tableSizeFor()函数,它会将容量置为 2的幂次,并保证不超过MAXIMUM_CAPACITY。其二就是调用扩容函数resize()进行容量翻倍时。如果容量达到MAXIMUM_CAPACITY时允许再扩容,新数组的容量就是 1 << 31,这会造成整型溢出,故Integer.MAX_VALUE是HashMap的最终容量。

HashMap的最大扩容阈值是2^31-1

在扩容函数resize()中有一个强制设置阈值大小的代码片段:

if (oldCap >= MAXIMUM_CAPACITY) {
    threshold = Integer.MAX_VALUE;
    return oldTab;
}

hreshold是HashMap所能容纳的最大数据量的Node(键值对)个数,threshold = length * Load factor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。

  在这里可以看到,其实 HashMap 扩容阈值threshold的最大值就是Integer.MAX_VALUE=2^31-1;

刷一道面试题

  今天看一个关于HashMap的性能问题:如果HashMap只装载100个元素,new HashMap(int x)中x的最佳值是多少,为什么?

  答案:256。

  解析:题意是令加载因子取默认值0.75,此时HashMap的初始容量可以设为100/0.75 = 133.33,向上取整为134。
无论你的HashMap(int x)中的x设置为多少,HashMap的大小都是2n,而且2n是大于x的第一个数,故大于134的第一个2^n无疑是256。

结束语

  以上就是这篇文章的全部内容了,希望本文对道友的学习或者工作能带来一定的帮助,如有疑问请留言交流。Wiener在此祝各位生活愉快!工作顺利!

  人情早晚有用完的时候,如若自己拥有足够的实力,定能赢得别人的尊重。更何况没有人欠我们人情呢!为人处世尚且如此,披星戴月的码农是不是要刻苦钻研,拓展技术栈的广度和深度呢?

Reference

posted @ 2022-04-16 20:18  楼兰胡杨  阅读(2060)  评论(0编辑  收藏  举报