精华内容
下载资源
问答
  • 一、HashMap 的初始化 ...回到 HashMap 的构造方法,threshold 为扩的阈值,在构造方法中由 tableSizeFor() 方法调整直接赋值,所以在构造 HashMap 时,如果传递 1000,threshold 调整的值确实是 1024,但

    欢迎大家关注我的公众号【老周聊架构】,Java后端主流技术栈的原理、源码分析、架构以及各种互联网高并发、高性能、高可用的解决方案。

    一、HashMap 的初始化

    关于HashMap 的初始化,可以参考我们上一篇说过:JDK8中的HashMap初始化和扩容机制

    二、HashMap 的 table 初始化

    这个问题也可以这样问,HashMap1000条数据,构造时传1000会不会让HashMap动态扩容?

    回到 HashMap 的构造方法,threshold 为扩容的阈值,在构造方法中由 tableSizeFor() 方法调整后直接赋值,所以在构造 HashMap 时,如果传递 1000threshold 调整后的值确实是 1024,但 HashMap 并不直接使用它。

    仔细想想就会知道,初始化时决定了 threshold 值,但其装载因子(loadFactor)并没有参与运算,那在后面具体逻辑的时候,HashMap 是如何处理的呢?

    HashMap 中,所有的数据,都是通过成员变量 table 数组来存储的,在 JDK 1.71.8 中虽然 table 的类型有所不同,但是数组这种基本结构并没有变化。那么 table、threshold、loadFactor 三者之间的关系,就是:table.size == threshold * loadFactor

    那这个 table 是在什么时候初始化的呢?这就要说说 HashMap 的扩容。

    HashMap 中,动态扩容的逻辑在 resize() 方法中。这个方法不仅仅承担了 table扩容,它还承担了 table初始化

    当我们首次调用 HashMapput() 方法存数据时,如果发现 tablenull,则会调用 resize() 去初始化 table,具体逻辑在 putVal() 方法中。

    在这里插入图片描述

    resize() 方法中,调整了最终 threshold 值,以及完成了 table 的初始化。

    在这里插入图片描述
    因为 resize() 还糅合了动态扩容的逻辑,所以我将初始化 table 的逻辑用注释标记出来了。其中 xxxCapxxxThr 分别对应了 table容量动态扩容阈值,所以存在两组数据。

    当我们指定了初始容量,且 table 未被初始化时,oldThr 就不为 0,则会走到代码 ① 的逻辑。在其中将 newCap 赋值为 oldThr,也就是新创建的 table 会是我们构造的 HashMap 时指定的容量值。

    之后会进入代码 ② 的逻辑,其中就通过装载因子(loadFactor)调整了新的阈值(newThr),当然这里也做了一些限制需要让 newThr 在一个合法的范围内。

    代码 ③ 中,将使用 loadFactor 调整后的阈值,重新保存到 threshold 中。并通过 newCap 创建新的数组,将其指定到 table 上,完成 table 的初始化(代码 ④)。

    到这里也就清楚了,虽然我们在初始化时,传递进来的 initialCapacity 虽然经过 tableSizeFor() 方法调整后,直接赋值给 threshold,但是它实际是 table 的尺寸,并且最终会通过 loadFactor 重新调整 threshold

    那么回到之前的问题就有答案了,虽然 HashMap 初始容量指定为 1000,会被 tableSizeFor() 调整为 1024,但是它只是表示 table 数组为 1024,扩容的重要依据扩容阈值会在 resize() 中调整为 768(1024 * 0.75)

    它是不足以承载 1000 条数据的,最终在存够 1k 条数据之前,还会触发一次动态扩容


    Question: 那构造时传多少才能让HashMap存1000条数据不需要动态扩容呢?

    我们可以反推一下:

    thresholdNew * 0.75 > 1000,则 thresholdNew > 1333.3

    而我们上面分析构造传1000的时候,thresholdNew 会被 tableSizeFor() 调整为 10241024 < 1333.3不满足。

    又我们知道了 tableSizeFor() 这个方法返回大于输入参数且最接近的2的整数次幂的数,则我们构造时传入1024~2048之间的数,就会保证HashMap存1000条数据不需要动态扩容

    三、举一反三

    Question: 那构造时传10000是否能让HashMap存10000条数据不需要动态扩容呢?

    答案是可以的。

    what?上面1000的就不行,10000就可以满足不需要动态扩容了?

    别着急,按照我们上面分析的一步一步来。

    当我们构造传10000时,实际上经过 tableSizeFor() 方法处理之后,就会变成 214 次幂 16384,再算上负载因子 0.75f,实际在不触发扩容的前提下,可存储的数据容量是 12288(16384 * 0.75f)。完全可以存储10000条数据。

    四、小结

    • HashMap 构造方法传递的 initialCapacity,虽然在处理后被存入了 threshold 中,但它实际表示 table 的容量。
    • 构造方法传递的 initialCapacity,最终会被 tableSizeFor() 方法动态调整为 2N 次幂,以方便在扩容的时候,计算数据在 newTable 中的位置。
    • 如果设置了 table 的初始容量,会在初始化 table 时,将扩容阈值 threshold 重新调整为 table.size * loadFactor
    • HashMap 是否扩容,由 threshold 决定,而 threshold 又由初始容量loadFactor 决定。
    • 如果我们预先知道 HashMap 数据量范围,可以预设 HashMap 的容量值来提升效率,但是需要注意要考虑装载因子的影响,才能保证不会触发预期之外的动态扩容。
    展开全文
  • HashMap扩容后,元素是如何重新分布的

    千次阅读 多人点赞 2020-09-10 11:39:08
    2、当元素增多导致扩之后,元素是如何重新分布的 同样,为了方便读者复盘,我截取源码是尽量将行号带上。 jdk版本还是1.8 结构图 再重复一遍,HashMap的底层数据结构为数组+链表+红黑树的结构,放一个HashMap的...

    上文回顾

    在上文深入源码分析HashMap到底是怎样将元素put进去的

    我们着重分析了无参构造函数是如何创建map对象和HashMap是如何将第一个元素puttable的。

    此篇重点

    这篇我们将逐行代码分析

    1、有参构造函数是如何创建map对象的
    2、当元素增多导致扩容之后,元素是如何重新分布的

    同样,为了方便读者复盘,我截取源码是尽量将行号带上。

    jdk版本还是1.8

    结构图

    再重复一遍,HashMap的底层数据结构为数组+链表+红黑树的结构,放一个HashMap的结构示意图,有个大致印象。
    在这里插入图片描述

    解剖思路

    创建一个有参构造函数,并往其中添加若干元素,直至触发扩容机制

    为了方便方便计算hash值,key和value都选用比较小的字符串

    关于调试键的使用请参照:IDEA调试键的说明,在此不再赘诉

    调试代码

    public static void main(String[] args) {
    
            System.out.println("★★★★★★解剖开始★★★★★★");
    
            HashMap<String, String> map = new HashMap<>(12);
    
            map.put("1", "1");
            map.put("2", "2");
            map.put("3", "3");
            map.put("4", "4");
    
            // 实验key相同的情况
            map.put("4", "D");
    
            map.put("5", "5");
            map.put("6", "6");
            map.put("7", "7");
            map.put("8", "8");
            map.put("9", "9");
            map.put("10", "10");
            map.put("11", "11");
            map.put("12", "12");
    
            // 第一个扩容点
            map.put("13", "13");
            map.put("14", "14");
            map.put("15", "15");
            map.put("16", "16");
    
            map.put("17", "17");
            map.put("18", "18");
            map.put("19", "19");
            map.put("20", "20");
    
            map.put("21", "21");
            map.put("22", "22");
            map.put("23", "23");
            map.put("24", "24");
    
            //第二个扩容点
            map.put("25", "25");
    
    
        }
    
    

    断点打在第一行
    在这里插入图片描述

    以debug模式启动,开始解剖之旅

    点击Step OverHashMap<String, String> map = new HashMap<>(12);所在行

    点击Force Step Into,会发现先调用的是类加载器
    在这里插入图片描述
    这不是今天的重点,我们直接Step Out,然后再次点击Force Step Into,进入源码
    在这里插入图片描述
    调用的是双参构造函数DEFAULT_LOAD_FACTOR为0.75,initialCapacity为12,继续Force Step Into
    在这里插入图片描述
    上图来到双参构造函数,继续Force Step Into会发现依旧调用了父类的构造函数
    在这里插入图片描述
    掉完父类构造函数,继续双参构造函数,经过参数校验,源码456行之后,loadFactor=0.75,我们重点看看this.threshold = tableSizeFor(initialCapacity)
    在这里插入图片描述
    457Force Step Into,会发现跳转到tableSizeFor(int cap)
    在这里插入图片描述
    关于tableSizeFor(int cap),在以前的文章详细分析过,有兴趣的可以去看看 读HashMap源码之tableSizeFor,这里直接说结论,就是你给一个初始容量值,经过这个方法后,返回一个最接近该值的、且不小于该值本身的2^n的那个值,当然,最大不能超过static final int MAXIMUM_CAPACITY = 1 << 30即2的30次方

    所以这里传入12返回的应该是16,n = 15 ,n+1 = 16
    在这里插入图片描述
    所以看到这应该明白,管你传9 10 11 12 13 14 15 16,经过tableSizeFor后都是返回16,这样就确保了threshold总是2的n次方,后面就会发现,其实这个值不是给扩容阈值的,而是给map的初始容量

    继续Force Step Into回到双参函数,将tableSizeFor的结果赋值给threshold扩容阈值=16
    在这里插入图片描述

    开始 put

    到此初始化map完成,其实到了这一步,table还没建立,继续往下Force Step Into开始put
    在这里插入图片描述
    继续Force Step Into进入map.put("1", "1");,来到源码的put(K key, V value)

    在这里插入图片描述
    源码的put(K key, V value)又是调用的putVal,调用之前会计算一下key的hash值,进去看看
    在这里插入图片描述
    key.hashCode()调用的是StringhashCode
    在这里插入图片描述
    然后返回put方法进入putVal,继续Force Step Into,此时hash值为49
    在这里插入图片描述
    因为在初始化时,并没有看到有初始化table,所以此处的table肯定是null
    在这里插入图片描述
    那顺理成章的就要执行resize()了,继续Force Step Into,进入resize()
    在这里插入图片描述
    这个我们在上文 **深入源码分析HashMap到底是怎样将元素put进去的**已经分析了一次,鉴于比较复杂,就再分析一次,还是一样,代码执行路径用红框标记出来
    在这里插入图片描述
    直接返回newTab
    在这里插入图片描述
    通过对上面源码的走读,发现带参构造函数创建的map,

    初始容量就取决于源码692行的newCap = oldThr;

    oldThr又取决于源码680行的int oldThr = threshold;

    在这里插入图片描述
    还记得threshold怎么来的吗,源码457行的tableSizeFor(int initialCapacity),你说狗不狗,在这等你呢,
    所以tableSizeFor的真实目的是为了确保所有map初始化时容量均为2的n次幂
    在这里插入图片描述
    扯远了,回来,拉回来,继续Force Step Into,刚刚走到table创建完,即首次resize完成
    在这里插入图片描述
    有了数组了,长度也知道了,该考虑元素位置了,上文也详细讲解了,

    决定元素位置的是(n - 1) & hash表达式的结果,自己想动手算的参照上文的方法去算

    这里直接看结果,计算出i=1
    在这里插入图片描述
    因为是刚刚创建的,所以tab[1]自然为null,顺理成章的就执行tab[i] = newNode(hash, key, value, null);,构建一个链表的节点放入1号位
    在这里插入图片描述
    继续调试,执行完放入元素之后modCount自增,size自增,并和扩容阈值(当前是12)比较,1小于12不用扩容,

    执行完毕,关于modCount上文
    在这里插入图片描述

    自己画个示意图,大概就是这样的,只有1号位置有元素,其他的均为null
    在这里插入图片描述
    继续Force Step Into,继续map.put("2", "2");,这次算的hash值为50,但此时table不再为null,直接计算位置,1等于2,且该位置为null,直接构造一个Node放进去
    在这里插入图片描述
    依次类推,我们就不一一看了,直接Step Overmap.put("4", "D");,看看key值相同,value不同时怎么处理
    在这里插入图片描述
    一路Force Step Into,来到putVal,发现hash还是52,定位的i也是4,个位置已经有元素了,所以走了源码634

    仔细研究一下这行代码

    p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))
    

    p.hash == hashture(k = p.key) == key也为真so执行e = p;,然后暂时还没有树化,所以源码656行直接将新的value覆盖旧的的value
    在这里插入图片描述
    至此,覆盖问题解决,继续Force Step Into,后面没有值重复的,经过一路Force Step Into
    ,1-9位置示意图如下
    在这里插入图片描述

    一直putputput直到map.put("10", "10");,为什么到了这里停下来看呢,因为此时hash只不同了,位置大概率也会不同,进去看看
    在这里插入图片描述
    果然,此时hash已经变成了1567,比之前的递增大了很多,位置也变成了15,与此类似,11的位置为0
    在这里插入图片描述
    截止到目前已经放了11个元素,位置示意图如下
    前11个元素位置示意图
    理论上,再放一个就触及到了我们的扩容阈值threshold = 16*0.75 = 12,但看一下源码662
    在这里插入图片描述
    其实12的时候还不会,是要大于12才会,那就继续Force Step Into走进map.put("12", "12")
    在这里插入图片描述
    事情开始起变化,这hash值不同,但计算的位置i=1上却已经有了元素
    在这里插入图片描述
    上面红色框就是代码执行路径,源码642表明12节点被放在p节点即1号位置的next,而e在源码641赋值时p.next此时还是null,所以下面的代码路径是正确的
    在这里插入图片描述
    所以此时的key=1就有了next,此时示意图如下:
    在这里插入图片描述

    继续Force Step Into,发现前半段map.put("13", "13");还是和map.put("12", "12");一样在这里插入图片描述
    本来按照剧本,key=13应该被放到2号位置key=2next
    示意图应该如下
    在这里插入图片描述
    但是,万事就怕但是,在这里if (++size > threshold)不满足了
    在这里插入图片描述

    增量扩容

    他要再次执行resize()了进去瞧瞧,先不管元素移动,先看扩容
    在这里插入图片描述
    对比第一次的resize来看
    在这里插入图片描述

    元素迁移

    第二次的容量和阈值都比第一次大了2倍,且oldTab不再为null,需要将oldTab迁移到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;
                                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);
                            if (loTail != null) {
                                loTail.next = null;
                                newTab[j] = loHead;
                            }
                            if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
    

    回到正题,继续Force Step Into,看源码707行,for (int j = 0; j < oldCap; ++j)就是要循环整个旧表

    上面的代码分三种情况来读

    1. 一是位置上只有一个Node元素,即nextnull的,类似上面的3-9号位置上都只有一个元素
    2. 第二种一个位置上有多个元素的,类似上面的12号位置,目前都有两个元素
    3. 第三种就是此位置上的元素为TreeNode类型的,目前没有,今天先不考虑

    对于第一种情况,核心操作就是源码712行的newTab[e.hash & (newCap - 1)] = e;

    计算该元素在新表中的位置,e.hash & (newCap - 1)
    在这里插入图片描述
    所以0号元素经过e.hash & (newCap - 1)1568 & 31后,工具计算结果在新表的位置是0
    在这里插入图片描述
    然后第二个元素即1号元素,正好是第二种情况,示意图再看一下
    在这里插入图片描述
    源码709oldTab[1]不为null711e.next也不为null

    e instanceof TreeNode也是false,所以核心流程来到了719行的do……while
    在这里插入图片描述
    do……while这段单独拎出来看,先定义两个链表的头和尾,一个链表用来装与旧表元素位置相同的元素,一个用来装需要重新分配位置的元素

                            Node<K,V> loHead = null, loTail = null;// 位置相同的链表的头尾
                            Node<K,V> hiHead = null, hiTail = null;// 位置要移动的链表头尾
                            Node<K,V> next;
                            do {
                                next = e.next; 
                                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);
                            if (loTail != null) {
                                loTail.next = null;
                                newTab[j] = loHead;
                            }
                            if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;
                            }
    

    key=1元素开始执行时,源码721e.hash & oldCap46 & 16 != 0,跳转至源码729
    在这里插入图片描述
    继续循环1号位置,此时e.hash & oldCap1569 & 16结果为0,所以将e赋值给loHead,同时链表尾部loTail也指向e
    在这里插入图片描述
    由于只有两个元素,循环到此结束了

    最后将loHead放在newTab[1]即在新数组中与旧数组位置相同的地方

    hiHead则被放在新的数组newTab[1 + 16]即在旧数组位置基础上再加上旧数组的容量

    以此类推,2号位上的两个元素分别被放置在新表的2号和2+16号位置上

    最后操作下来,位置关系如示意图
    在这里插入图片描述

    总结

    到此目标完成,总结一下要点

    1、HashMap的初始化是在插入第一个元素时调用resize完成的(源码629行)
    2、不指定容量,默认容量为16(源码694
    3、指定容量不一定按照你的值来,会经过tableSizeFor转成不小于输入值的2的n次幂,源码378

    这里有个小问题,tableSizeFor转换成2的n次幂不是直接赋值给capacity,而是先将值暂时保存在threshold,见源码457,然后在put第一个元素resize时,婉转的传递给newCap
    在这里插入图片描述

    4、put元素时,元素的位置取决于数组的长度和key的hash值按位与的结果i = (n - 1) & hash源码630
    4.1 如果这里没有元素,直接放这
    4.2 如果有,判断是不是键冲突(源码634),直接新值覆盖旧值*(源码657
    4.3 如果有且不是键冲突,则将其放在原元素的next位置(源码642
    5、只有当size大于了扩容阈值size > threshold,才会触发扩容,源码662,扩容前,当前元素已经放好了
    6、扩容时,容量和扩容阈值都翻番(源码687),但要小于MAXIMUM_CAPACITY
    7、扩容时,元素在新表中的位置分情况
    7.1 当元素只是孤家寡人即元素的next==null(源码)711时,位置为e.hash & (newCap - 1)(源码712
    7.2 当元素有next节点时,该链表上的元素分两类

    7.21 e.hash & oldCap = 0的,在新表中与旧表中的位置一样(源码738

    7.22 e.hash & oldCap != 0的,位置为旧表位置+旧表容量,源码742

    在这里插入图片描述

    展望:

    调了一天,还只是调了其中的一部分,初始化、初始扩容,和增量扩容,类似树化、拆树还没研究呢

    构造树化的思路,也是从源码上找,主要是以下几行
    在这里插入图片描述只要你能让不同的key的hash值相同,并且key不相同,就可以制造出hash冲突,就能将多个元素放在同一个位置上,然后直至触发树化,具体情况我们下回分解。




    为了写这玩意,死了我多少脑细胞




    喝瓶八二年的矿泉水,不过分吧?我自己去买,你买单就行了

    --------------------> 买单 <-------------




    展开全文
  • // 预计存入 1w 条数据,初始化赋值 10000,避免 resize。 HashMap<String,String> map = new HashMap<...Java 集合的扩 HashMap 算是我们最常用的集合之一,虽然对于 Android 开发者,Google 官方推...
    // 预计存入 1w 条数据,初始化赋值 10000,避免 resize。
    HashMap<String,String> map = new HashMap<>(10000)
    // for (int i = 0; i < 10000; i++)

    Java 集合的扩容

    HashMap 算是我们最常用的集合之一,虽然对于 Android 开发者,Google 官方推荐了更省内存的 SparseArray 和 ArrayMap,但是 HashMap 依然是最常用的。

    我们通过 HashMap 来存储 Key-Value 这种键值对形式的数据,其内部通过哈希表,让存取效率最好时可以达到 O(1),而又因为可能存在的 Hash 冲突,引入了链表和红黑树的结构,让效率最差也差不过 O(logn)。

    整体来说,HashMap 作为一款工业级的哈希表结构,效率还是有保障的。

    编程语言提供的集合类,虽然底层还是基于数组、链表这种最基本的数据结构,但是和我们直接使用数组不同,集合在容量不足时,会触发动态扩容来保证有足够的空间存储数据

    动态扩容,涉及到数据的拷贝,是一种「较重」的操作。那如果能够提前确定集合将要存储的数据量范围,就可以通过构造方法,指定集合的初始容量,来保证接下来的操作中,不至于触发动态扩容。

    这就引入了本文开篇的问题,如果使用 HashMap,当初始化是构造函数指定 1w 时,后续我们立即存入 1w 条数据,是否符合与其不会触发扩容呢?

    在分析这个问题前,那我们先来 看看,HashMap 初始化时,指定初始容量值都做了什么?

    PS:本文所涉及代码,均以 JDK 1.8 中 HashMap 的源码举例。

    HashMap 的初始化

    在 HashMap 中,提供了一个指定初始容量的构造方法 HashMap(int initialCapacity),这个方法最终会调用到 HashMap 另一个构造方法,其中的参数 loadFactor 就是默认值 0.75f。

    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);
    }

    其中的成员变量 threshold 就是用来存储,触发 HashMap 扩容的阈值,也就是说,当 HashMap 存储的数据量达到 threshold 时,就会触发扩容。

    从构造方法的逻辑可以看出,HashMap 并不是直接使用外部传递进来的 initialCapacity,而是经过了 tableSizeFor() 方法的处理,再赋值到 threshole 上。

    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;
    }

    tableSizeFor() 方法中,通过逐步位运算,就可以让返回值,保持在 2 的 N 次幂。以方便在扩容的时候,快速计算数据在扩容后的新表中的位置。

    那么当我们从外部传递进来 1w 时,实际上经过 tableSizeFor() 方法处理之后,就会变成 2 的 14 次幂 16384,再算上负载因子 0.75f,实际在不触发扩容的前提下,可存储的数据容量是 12288(16384 * 0.75f)。

    这种场景下,用来存放 1w 条数据,绰绰有余了,并不会触发我们猜想的扩容。

    HashMap 的 table 初始化

    当我们把初始容量,调整到 1000 时,情况又不一样了,具体情况具体分析。

    再回到 HashMap 的构造方法,threshold 为扩容的阈值,在构造方法中由 tableSizeFor() 方法调整后直接赋值,所以在构造 HashMap 时,如果传递 1000,threshold 调整后的值确实是 1024,但 HashMap 并不直接使用它。

    仔细想想就会知道,初始化时决定了 threshold 值,但其装载因子(loadFactor)并没有参与运算,那在后面具体逻辑的时候,HashMap 是如何处理的呢?

    在 HashMap 中,所有的数据,都是通过成员变量 table 数组来存储的,在 JDK 1.7 和 1.8 中虽然 table 的类型有所不同,但是数组这种基本结构并没有变化。那么 table、threshold、loadFactor 三者之间的关系,就是:

    table.size == threshold * loadFactor

    那这个 table 是在什么时候初始化的呢?这就要说会到我们一直在回避的问题,HashMap 的扩容。

    在 HashMap 中,动态扩容的逻辑在 resize() 方法中。这个方法不仅仅承担了 table 的扩容,它还承担了 table 的初始化。

    当我们首次调用 HashMap 的 put() 方法存数据时,如果发现 table 为 null,则会调用 resize() 去初始化 table,具体逻辑在 putVal() 方法中。

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length; // 调用 resize()
        // ...
    }

    resize() 方法中,调整了最终 threshold 值,以及完成了 table 的初始化。

    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; 
        }
        else if (oldThr > 0) 
            newCap = oldThr; // ①
        else {               
            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; // ③
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab; // ④
        // ....
    }

    注意看代码中的注释标记。

    因为 resize() 还糅合了动态扩容的逻辑,所以我将初始化 table 的逻辑用注释标记出来了。其中 xxxCap 和 xxxThr 分别对应了 table 的容量和动态扩容的阈值,所以存在旧和新两组数据。

    当我们指定了初始容量,且 table 未被初始化时,oldThr 就不为 0,则会走到代码 的逻辑。在其中将 newCap 赋值为 oldThr,也就是新创建的 table 会是我们构造的 HashMap 时指定的容量值。

    之后会进入代码 的逻辑,其中就通过装载因子(loadFactor)调整了新的阈值(newThr),当然这里也做了一些限制需要让 newThr 在一个合法的范围内。

    在代码 中,将使用 loadFactor 调整后的阈值,重新保存到 threshold 中。并通过 newCap 创建新的数组,将其指定到 table 上,完成 table 的初始化(代码 )。

    到这里也就清楚了,虽然我们在初始化时,传递进来的 initialCapacity 虽然被赋值给 threshold,但是它实际是 table 的尺寸,并且最终会通过 loadFactor 重新调整 threshold

    那么回到之前的问题就有答案了,虽然 HashMap 初始容量指定为 1000,但是它只是表示 table 数组为 1000,扩容的重要依据扩容阈值会在 resize() 中调整为 768(1024 * 0.75)。

    它是不足以承载 1000 条数据的,最终在存够 1k 条数据之前,还会触发一次动态扩容。

    通常在初始化 HashMap 时,初始容量都是根据业务来的,而不会是一个固定值,为此我们需要有一个特殊处理的方式,就是将预期的初始容量,再除以 HashMap 的装载因子,默认时就是除以 0.75。

    例如想要用 HashMap 存放 1k 条数据,应该设置 1000 / 0.75,实际传递进去的值是 1333,然后会被 tableSizeFor() 方法调整到 2048,足够存储数据而不会触发扩容。

    当想用 HashMap 存放 1w 条数据时,依然设置 10000 / 0.75,实际传递进去的值是 13333,会被调整到 16384,和我们直接传递 10000 效果是一样的。

    小结时刻

    到这里,就了解清楚了 HashMap 的初始容量,应该如何科学的计算,本质上你传递进去的值可能并无法直接存储这么多数据,会有一个动态调整的过程。其中就需要将我们预期的值进行放大,比较科学的就是依据装载因子进行放大。

    最后我们再总结一下:

    1. HashMap 构造方法传递的 initialCapacity,虽然在处理后被存入了 loadFactor 中,但它实际表示 table 的容量。
    2. 构造方法传递的 initialCapacity,最终会被 tableSizeFor() 方法动态调整为 2 的 N 次幂,以方便在扩容的时候,计算数据在 newTable 中的位置。
    3. 如果设置了 table 的初始容量,会在初始化 table 时,将扩容阈值 threshold 重新调整为 table.size * loadFactor。
    4. HashMap 是否扩容,由 threshold 决定,而 threshold 又由初始容量和 loadFactor 决定。
    5. 如果我们预先知道 HashMap 数据量范围,可以预设 HashMap 的容量值来提升效率,但是需要注意要考虑装载因子的影响,才能保证不会触发预期之外的动态扩容。

    HashMap 作为 Java 最常用的集合之一,市面上优秀的文章很多,但是很少有人从初始容量的角度来分析其中的逻辑,而初始容量又是集合中比较实际的优化点。其实不少人也搞不清楚,在设置 HashMap 初始容量时,是否应该考虑装载因子,才有了此文。

    如果本文对你有所帮助,留言、转发、点好看是最大的支持,谢谢!


    公众号后台回复成长『成长』,将会得到我准备的学习资料,也能回复『加群』,一起学习进步。

    展开全文
  • https://juejin.im/post/6844903983748743175 https://www.jianshu.com/p/6d7151c2a700 //预计存入1w条数据,初始化赋值10000,避免resize。 HashMap<String,String>...Java 集合的扩 Ha...

    https://juejin.im/post/6844903983748743175

    https://www.jianshu.com/p/6d7151c2a700

     

     

    // 预计存入 1w 条数据,初始化赋值 10000,避免 resize。
    HashMap<String,String> map = new HashMap<>(10000)
    // for (int i = 0; i < 10000; i++)
    复制代码

     

    Java 集合的扩容

    HashMap 算是我们最常用的集合之一,虽然对于 Android 开发者,Google 官方推荐了更省内存的 SparseArray 和 ArrayMap,但是 HashMap 依然是最常用的。

    我们通过 HashMap 来存储 Key-Value 这种键值对形式的数据,其内部通过哈希表,让存取效率最好时可以达到 O(1),而又因为可能存在的 Hash 冲突,引入了链表和红黑树的结构,让效率最差也差不过 O(logn)。

    整体来说,HashMap 作为一款工业级的哈希表结构,效率还是有保障的。

    编程语言提供的集合类,虽然底层还是基于数组、链表这种最基本的数据结构,但是和我们直接使用数组不同,集合在容量不足时,会触发动态扩容来保证有足够的空间存储数据

    动态扩容,涉及到数据的拷贝,是一种「较重」的操作。那如果能够提前确定集合将要存储的数据量范围,就可以通过构造方法,指定集合的初始容量,来保证接下来的操作中,不至于触发动态扩容。

    这就引入了本文开篇的问题,如果使用 HashMap,当初始化是构造函数指定 1w 时,后续我们立即存入 1w 条数据,是否符合与其不会触发扩容呢?

    在分析这个问题前,那我们先来看看,HashMap 初始化时,指定初始容量值都做了什么?

    PS:本文所涉及代码,均以 JDK 1.8 中 HashMap 的源码举例。

    HashMap 的初始化

    在 HashMap 中,提供了一个指定初始容量的构造方法 HashMap(int initialCapacity),这个方法最终会调用到 HashMap 另一个构造方法,其中的参数 loadFactor 就是默认值 0.75f。

    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);
    }
    复制代码

    其中的成员变量 threshold 就是用来存储,触发 HashMap 扩容的阈值,也就是说,当 HashMap 存储的数据量达到 threshold 时,就会触发扩容。

    从构造方法的逻辑可以看出,HashMap 并不是直接使用外部传递进来的 initialCapacity,而是经过了 tableSizeFor() 方法的处理,再赋值到 threshole 上。

    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;
    }
    复制代码

    tableSizeFor() 方法中,通过逐步位运算,就可以让返回值,保持在 2 的 N 次幂。以方便在扩容的时候,快速计算数据在扩容后的新表中的位置。

    那么当我们从外部传递进来 1w 时,实际上经过 tableSizeFor() 方法处理之后,就会变成 2 的 14 次幂 16384,再算上负载因子 0.75f,实际在不触发扩容的前提下,可存储的数据容量是 12288(16384 * 0.75f)。

    这种场景下,用来存放 1w 条数据,绰绰有余了,并不会触发我们猜想的扩容。

     

    HashMap 的 table 初始化

    当我们把初始容量,调整到 1000 时,情况又不一样了,具体情况具体分析。

    再回到 HashMap 的构造方法,threshold 为扩容的阈值,在构造方法中由 tableSizeFor() 方法调整后直接赋值,所以在构造 HashMap 时,如果传递 1000,threshold 调整后的值确实是 1024,但 HashMap 并不直接使用它。

    仔细想想就会知道,初始化时决定了 threshold 值,但其装载因子(loadFactor)并没有参与运算,那在后面具体逻辑的时候,HashMap 是如何处理的呢?

    在 HashMap 中,所有的数据,都是通过成员变量 table 数组来存储的,在 JDK 1.7 和 1.8 中虽然 table 的类型有所不同,但是数组这种基本结构并没有变化。那么 table、threshold、loadFactor 三者之间的关系,就是:

    table.size == threshold * loadFactor

    那这个 table 是在什么时候初始化的呢?这就要说会到我们一直在回避的问题,HashMap 的扩容。

    在 HashMap 中,动态扩容的逻辑在 resize() 方法中。这个方法不仅仅承担了 table 的扩容,它还承担了 table 的初始化。

    当我们首次调用 HashMap 的 put() 方法存数据时,如果发现 table 为 null,则会调用 resize() 去初始化 table,具体逻辑在 putVal() 方法中。

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length; // 调用 resize()
        // ...
    }
    复制代码

    resize() 方法中,调整了最终 threshold 值,以及完成了 table 的初始化。

    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; 
        }
        else if (oldThr > 0) 
            newCap = oldThr; // ①
        else {               
            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; // ③
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab; // ④
          // ....
    }
    复制代码

    注意看代码中的注释标记。

    因为 resize() 还糅合了动态扩容的逻辑,所以我将初始化 table 的逻辑用注释标记出来了。其中 xxxCap 和 xxxThr 分别对应了 table 的容量和动态扩容的阈值,所以存在旧和新两组数据。

    当我们指定了初始容量,且 table 未被初始化时,oldThr 就不为 0,则会走到代码 的逻辑。在其中将 newCap 赋值为 oldThr,也就是新创建的 table 会是我们构造的 HashMap 时指定的容量值。

    之后会进入代码 的逻辑,其中就通过装载因子(loadFactor)调整了新的阈值(newThr),当然这里也做了一些限制需要让 newThr 在一个合法的范围内。

    在代码 中,将使用 loadFactor 调整后的阈值,重新保存到 threshold 中。并通过 newCap 创建新的数组,将其指定到 table 上,完成 table 的初始化(代码 )。

    到这里也就清楚了,虽然我们在初始化时,传递进来的 initialCapacity 虽然经过 tableSizeFor() 方法调整后,直接赋值给 threshold,但是它实际是 table 的尺寸,并且最终会通过 loadFactor 重新调整 threshold

    那么回到之前的问题就有答案了,虽然 HashMap 初始容量指定为 1000,会被 tableSizeFor() 调整为 1024,但是它只是表示 table 数组为 1024,扩容的重要依据扩容阈值会在 resize() 中调整为 768(1024 * 0.75)。

    它是不足以承载 1000 条数据的,最终在存够 1k 条数据之前,还会触发一次动态扩容。

     

    通常在初始化 HashMap 时,初始容量都是根据业务来的,而不会是一个固定值,为此我们需要有一个特殊处理的方式,就是将预期的初始容量,再除以 HashMap 的装载因子,默认时就是除以 0.75。

    例如想要用 HashMap 存放 1k 条数据,应该设置 1000 / 0.75,实际传递进去的值是 1333,然后会被 tableSizeFor() 方法调整到 2048,足够存储数据而不会触发扩容。

    当想用 HashMap 存放 1w 条数据时,依然设置 10000 / 0.75,实际传递进去的值是 13333,会被调整到 16384,和我们直接传递 10000 效果是一样的。

    小结时刻

    到这里,就了解清楚了 HashMap 的初始容量,应该如何科学的计算,本质上你传递进去的值可能并无法直接存储这么多数据,会有一个动态调整的过程。其中就需要将我们预期的值进行放大,比较科学的就是依据装载因子进行放大。

    最后我们再总结一下:

    1. HashMap 构造方法传递的 initialCapacity,虽然在处理后被存入了 loadFactor 中,但它实际表示 table 的容量。
    2. 构造方法传递的 initialCapacity,最终会被 tableSizeFor() 方法动态调整为 2 的 N 次幂,以方便在扩容的时候,计算数据在 newTable 中的位置。
    3. 如果设置了 table 的初始容量,会在初始化 table 时,将扩容阈值 threshold 重新调整为 table.size * loadFactor。
    4. HashMap 是否扩容,由 threshold 决定,而 threshold 又由初始容量和 loadFactor 决定。
    5. 如果我们预先知道 HashMap 数据量范围,可以预设 HashMap 的容量值来提升效率,但是需要注意要考虑装载因子的影响,才能保证不会触发预期之外的动态扩容。

    HashMap 作为 Java 最常用的集合之一,市面上优秀的文章很多,但是很少有人从初始容量的角度来分析其中的逻辑,而初始容量又是集合中比较实际的优化点。其实不少人也搞不清楚,在设置 HashMap 初始容量时,是否应该考虑装载因子,才有了此文。

    如果本文对你有所帮助,留言、转发、收藏是最大的支持,谢谢!

     

    展开全文
  • 以方便在扩容的时候,快速计算数据在扩容后的新表中的位置。 那么当我们从外部传递进来 1w 时,实际上经过 tableSizeFor() 方法处理之后,就会变成 2 的 14 次幂 16384,再算上负载因子 0.75f,实际在不触发扩容的...
  • ArrayList扩处理

    千次阅读 2018-11-29 21:09:03
    ArrayList是基于动态数组实现的一个数据结构,如果添加元素时,元素个数超过list的容量大小时,会涉及到扩。   ArrayList的扩是如何做的,跟着代码走最容易懂。 /** * 添加元素在list尾部 * * @param e...
  • ArrayList扩问题

    万次阅读 2018-12-14 15:19:30
    然后还要再进行一步判断,即判断当前新容量是否超过最大的容量 if (newCapacity - MAX_ARRAY_SIZE > 0),如果超过,则调用hugeCapacity方法,进去的是minCapacity,即新增元素需要的最小容量。获取newCapacity...
  • 浅谈ArrayList动态扩

    万次阅读 多人点赞 2017-10-23 22:54:24
    ArrayList实现了List接口,继承了AbstractList,底层是数组实现的,一般我们把它认为是可以自增扩的数组。它是非线程安全的,一般多用于单线程环境下(与Vector最大的区别就是,Vector是线程安全的,所以ArrayList ...
  • 今天刚好遇到一个关于集合扩的问题,正好借机整理一下: 当底层实现涉及到扩时,容器或重新分配一段更大的连续内存(如果是离散分配则不需要重新分配,离散分配都是插入新元素时动态分配内存),要将容器原来...
  • 上文讲解过自动迁移槽实现集群扩(传送门) 1.准备新节点 安装redis,参考传送门 节点配置,参考传送门 2.将节点加入集群 redis-cli --cluster add-node {new host}:{new port} {exist host}:{exist port} 加入...
  • jdk1.8 HashMap工作原理和扩机制(源码解析)

    万次阅读 多人点赞 2018-05-29 20:37:14
    容后得到了一个table的节点(Node)数组,接着根据传入的hash值去获得一个对应节点p并去判断是否为空,是的话就存入一个新的节点(Node)。反之如果当前存放的位置已经有值了就会进入到else中去。接着根据前面得到...
  • ArrayList自动扩原理

    2019-06-28 16:56:45
    ArrayList自动扩原理 一、ArrayList三种初始化 1、默认的构造器,将会以默认的大小来初始化内部的数组 public ArrayList(); 2、用一个ICollection对象来构造,并将该集合的元素添加到ArrayList public ...
  • Hadoop Hdfs扩

    千次阅读 2019-03-04 15:32:27
    代码同步 通过scp方式把现生产环境的Hadoop到10.3.5.123、10.3.5.124、10.3.5.125上。 关闭防火墙 [root@zfr ~]# service iptables stop 临时关闭,重启失效 [root@zfr ~]# chkconfig iptables off ...
  • Redis扩及Slot Balance、Reshard

    千次阅读 2018-09-19 15:57:37
    Redis扩及slot balance 新添加了两台机器 10.255.1.4 tbds-10-255-1-4 10.255.1.12 tbds-10-255-1-12 1、查看原有redis集群 登陆10.255.1.10原有redis的集群 ./redis-cli -c -h tbds-10-255-1-10 -p 6379 -a...
  • jdk1.8ArrayList主要方法和扩机制(源码解析)

    万次阅读 多人点赞 2018-05-31 14:17:17
    ArrayList简介: ArrayList实现了List接口它是一个可调整大小的数组可以用来存放各种形式的数据。并提供了包括CRUD在内的多种方法可以对数据进行操作但是它不是线程安全的,外ArrayList按照...//数组默认初始...
  • 电容式声器的输出阻抗呈性,因电容量小,但低频时容抗会很大。为保证低频的灵敏度,应有一个输入阻抗大于或等于声器输出阻抗的阻抗变换器与其相连,经阻抗变换,再用传输线与放大器相连。这个阻抗变换器一般...
  • ArrayList的底层是一个动态数组,ArrayList首先会对进来的初始化参数initalCapacity进行判断: ...当数组的大小大于初始容量的时候(比如初始为10,当添加第11个元素的时候),就会进行扩,新的容量为旧的容量...
  • 【项目4-为动态数组扩】 下面的程序,利用动态数组保存学生的成绩。当再有一批学生成绩需要保存时,要为之扩(和吃自助一样,用多少,取多少,这好),请补充完整下面的程序,实现如图所示的功能。int ma
  • Redis集群提供了灵活的节点扩和收缩方案。在不影响集群对外服务的情况下,可以为集群添加节点进行扩也可以下线部分节点进行缩容。扩与缩容的本质是,对Redis的槽进行重新的分配。  集群现有6379、6380、6381...
  • 纳兰若的记忆

    千次阅读 2010-12-23 12:43:00
    人生若只如初见,何事秋风悲画扇? 等闲变却故人心,却道故人心易变。 骊山语罢清宵半,泪雨零铃终不怨。 何如薄倖锦衣郎,比翼连枝...若为其字 一、纳兰性德生平 纳兰性德于顺治十
  • 简单了解java的Vector扩

    千次阅读 2019-08-27 14:19:34
    package com.example.demo.test; import java.util.Vector; /** * <p> * <code>VectorTest<... * 测试Vector的扩方式,通过测试可以看出其每次以2的倍数形式扩充Object数组 ...
  • Redis源码学习——Redis字典扩问题

    千次阅读 2019-04-23 14:41:33
    通过上一篇对dictScan函数的分析,我们引出了两个问题,就是Redis字典在进行扩的时候,会从size=8直接扩到size=64吗?那段代码块真的有用吗?下面我们就通过查看源码,逐步来探索一下这个问题。 想要探索这个...
  •  超大文件上传就无法避免断点上传,想要实现秒你就饶不开文件特征。本文不涉及具体代码。...5、合并完成,后台的特征对比   看似原理简单,粗狂分析一下就可以动手了,其实每一步都有很多技术难点于...
  • 【转】纳兰

    千次阅读 热门讨论 2010-10-10 10:38:00
    <br />拟古决绝词 <br />人生若只如初见,何事秋风悲画扇? 等闲变却故人心,却道故人心易变。 骊山语罢清宵半,泪雨零铃终不怨。 何如薄倖锦衣郎,比翼连枝当日愿。...若为
  • ArrayList扩机制(基于jdk1.8)

    万次阅读 多人点赞 2019-03-28 17:07:13
    在讲扩机制之前,我们需要了解一下ArrayList中最主要的几个变量: //定义一个空数组以供使用 private static final Object[] EMPTY_ELEMENTDATA = {}; //也是一个空数组,跟上边的空数组不同之处在于,这个是在默....
  • 本文主要介绍了树莓派购买的配置方法,并通过samba以及aria2简单搭建带有下载功能的家庭nas服务器。
  • 懒得再一张张了, csdn太无聊了我放gihub上的图片都不让显示 esxi虚拟机分为3种磁盘策略 厚置备快速置零 厚置备延迟置零 精简置备 各有各的好处(也各有各的坑), 不过这不是本文的重点, 本文主要说说如何给...
  • HashMap的扩机制---resize()

    万次阅读 多人点赞 2016-11-24 10:20:17
    什么时候扩:当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值---即当前数组的长度乘以加载因子的值的时候,就要自动扩啦。 扩(resize)就是重新计算容量,向HashMap对象里不停的添加...
  • java7和java8 hashmap扩机制及区别

    千次阅读 2020-06-23 17:50:52
    (一) Java 7 中Hashmap扩机制 一、什么时候扩: 网上总结的会有很多,但大多都总结的不够完整或者不够准确。大多数可能值说了满足我下面条件一的情况。 扩必须满足两个条件: 1、 存放新值的时候当前已...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 38,509
精华内容 15,403
关键字:

容后传