HashMap 容量设置-什么时候初始化

1,082 阅读7分钟

前言

最近 在看 HashMap 看到一个问题:
HashMap 初始容量设置为 10000 时,放入 10000 条数据是否需要扩容;如果初始容量设置为 1000 时,放入 1000 条数据是否需要扩容?

不知道有多少小伙伴 能回答出这个问题哈

问题剖析

看到这个问题 我一开始也不知道,但是从题目中 我们可以看出 初始容量的设置大小 对后面的 存放数据的多少 一定有影响 不然问题也不会这么问。
我们知道 HashMap 中存在一个 负载因子loadFactor 正常境况下 我们都是使用的默认值0.75. 具体为什么是0.75 这个是前人的研究结果 根据泊松分布的结果 当负载因子是0.75的时候 Hash 的冲突 和 Hash 的扩容 权衡下来 是最好的。
当你的负载因 设置小了的时候 会导致Hash表中的存储容量变小 导致Hash 会频繁的扩容 我们都知道扩容的成本是很高的 Hash表中存储元素的位置 会发生改变 会频繁的移动元素。

那当我们设置大的时候 比如 设置成1,这样容量虽然变大 但是会导致hash 冲突增多,为什么会这样呢? 我们想一下 如果Hash的容量是16 我们的loadFactor设置成1的话 那我们的临界值 就是threshold 也会是16,这也就是意味着 当我们元素存放满的时候 才会去扩容 极端情况下 16个元素 刚好挨个存放到每个位置上 ,否则 必然会发生hash冲突,但元素越来越多的时候 元素存的越多 必然会造成冲突越多!

好的 上面的 有3个名词 我们需要注意下

  • 负载因子loadFactor
  • 临界值 threshold
  • 容量 capacity

源码分析

初始化

   static final int MAXIMUM_CAPACITY = 1 << 30;//最大的容量 2的31次方
   static final float DEFAULT_LOAD_FACTOR = 0.75f;
   
    <!--DEFAULT_LOAD_FACTOR 是默认值0.75f-->
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
   
    <!--这个是HashMap其中一个构造函数方法-->
    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);
    }
    
     /**
     * Returns a power of two size for the given target capacity.
     * 这个方法  写了一堆  最终的意思 知道下就行 就是找到一个离target capacity 就是目标容量 最接近的2的n次方的数字
     * 比如 capacity是10  那这个方法的结果就是16  
     * 如果 capacity是1000 那结果就是1024  
     * 如果 capacity是10000 那结果就是16384 
     */
    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;
    }

看下 上面的注释 如果这个时候 initialCapacity是1000的话 那最终算出的threshold是1024,如果是initialCapacity是10000的话 这个时候的threshold是16384

这个时候 我很好奇 初始化的时候 怎么没有个HashMap 赋值初始容量呢,其实HashMap 算是一个懒加载,初始化容量的时候 是在第一次新增元素的时候 发生的 我们去看下代码 一看究竟

存放元素

首先看下代码


    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        
        <!--主要看这边  这个是第一put 元素会执行的地方  主要方法就是看resize()-->
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
            
        <!--这边也可以关注下 HashMap 是怎么存值的 n是tab的容量-1 hash是key的hash后的值  2个数值相与后 得到存放的位置-->
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
            
        else {
            Node<K,V> e; K k;
            <!--这边的操作是 如果当前算出的位置的元素的hash值和我们的key的hash值相同 -->
            <!--如果Hash 相同 并且 数值相同 这里的判断 用的是== 或者equals方法  这边是一个知识点 -->
            <!--如果以上的相同  就直接替换-->
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
                
            <!--这边判断的 是如果P是一个红黑树 的处理   -->
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            <!--如果不是树结构  那就是链表结构  执行的逻辑是 循环链表 如果hash值和key数值相同 就替换链表中的元素   -->
            <!--如果匹配不到 就放入 链表的尾部节点  -->
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {// 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;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

注释 中 也简单说明了 HashMap 是怎么样存放元素的 有兴趣的 看看
现在还是回到我们的正题 关注下我们关系的resize()方法

resize()

代码 如下:

 final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;//第一次插入的时候 oldTab 是null oldCap值就是0
        int oldThr = threshold;//临界值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
        }
        
        <!--第一次的时候 应该进入这个判断里面  下面的因为注释也说了 这个是 initial capacity 的-->
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;//此时的newCap值 就是我们初始化的时候 临界值threshold
            
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        <!--newThr 没有被赋值 此时是0 进入下面的判断-->
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            <!--下面的等式 就是得到新的临界值newThr   就是 我们的初始值算出的threshold*负载因子 -->
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;//赋值 新的临界值 
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//初始化 Node数组
        table = newTab;
        
        <!--后面有一段是HashMap 扩容的处理  我就不细说了 具体讲的是 怎么把oldTab 元素 放入新的tab 中-->
        return newTab;
    }

从上面的 方法 我们可以得到 初始化得到的threshold 临界值 最终成了数组的capacity 也就是数组的大小 而新的临界值 其实就是 初始的临界值*负载因子
好的 描述完上面的关系

我们在看下 最初的题目:HashMap 初始容量设置为 10000 时,放入 10000 条数据是否需要扩容;如果初始容量设置为 1000 时,放入 1000 条数据是否需要扩容?

  • 初始容量10000的时候 算出初始的threshold是16384 最终得到的数组大小是capacity也是16384 最终的threshold临界值是16384*0.75=12288 如果这个时候 存入10000数据的时候 显然 没有达到临界值 不会触发扩容
  • 初始容量1000的时候 算出初始的threshold是1024 最终得到的数组大小是capacity也是1024 最终的threshold临界值是1024*0.75=768 如果这个时候 存入1000数据的时候 显然 已经超过临界值大小 会触发扩容

总结

HashMap 在面试题中的 出现概率 极高 有的时候 面试官 问的时候 不会那么直接,比如上面的问题 考察的就是 我们对HashMap 容量初始化的 了解程度 .
==HashMap 的容量计算是在第一次存放元素的时候执行的 get 这个点 我相信你能明白很多==