关于hashmap:HashMap相关

1、JDK 8 HashMap为啥要引入红黑树?当HashMap 的 key 抵触过多时,比方咱们应用了不好的 hash 算法,导致key抵触率极高,链表里会有很多数据。然而链表的查找性能很差,所以引入红黑树是为了优化查问性能。 2、JDK 8 HashMap为啥不间接用红黑树?因为树节点所占用的空间是一般节点的两倍,所以只有当节点足够多的时候,才会应用树节点。也就是说,最开始应用链表的时候,链表是比拟短的,空间占用也是比拟少的,查问性能都差不多。然而当链表越来越长,链表查问越来越慢,这时候才会舍弃链表而应用红黑树,以空间换工夫。所以没有必要一开始就用红黑树,另外,链表较长的状况十分少见。一开始就应用红黑树反而会导致所有的状况都会占用比链表大2倍的空间,事与愿违,这也是一种均衡的策略。 3、为什么链表长度达到8?因为达到8个元素的时候,概率曾经很低了。此时树化,性价比会很高。既不会因为链表太长(8)导致复杂度加大,也不会因为概率太高导致太多节点树化。 4、为什么红黑树转链表的阈值为6?次要是因为,如果也将该阈值设置于8,那么当hash碰撞在8时,会反生链表和红黑树的不停互相激荡转换。两头有个差值7能够避免链表和树之间的频繁转换。假如一下:如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果HashMap不停的插入,删除元素,链表个数在8左右彷徨,就会频繁的产生红黑树转链表,链表转红黑树,效率会很低下。 5、putVal 办法次要做了这么几件事件当桶数组 table 为空时,resize()初始化 table查找要插入的键值对是否曾经存在,onlyIfAbsent如果为false且oldValue等于null 则笼罩旧值。如果不存在,则将键值对链入链表中,如果链表长度达到了8,且数组长度小于64,那么就从新散列。如果大于64,则创立红黑树判断键值对数量是否大于阈值,大于的话则进行扩容操作。

June 1, 2023 · 1 min · jiezi

关于hashmap:HashMap

JDK 8 HashMap为啥要引入红黑树?当HashMap 的 key 抵触过多时,比方咱们应用了不好的 hash 算法,导致 key抵触率极高,咱们都晓得链表的查找性能很差,所以引入红黑树是为了优化查问性能。 JDK 8 HashMap为啥不间接用红黑树?因为树节点所占用的空间是一般节点的两倍,所以只有当节点足够多的时候,才会应用树节点。也就是说,最开始应用链表的时候,链表是比拟短的,空间占用也是比拟少的,查问性能都差不多,然而当链表越来越长,链表查问越来越慢,为了保障查问效率,这时候才会舍弃链表而应用红黑树,以空间换工夫。 所以没有必要一开始就用红黑树,另外,链表较长的状况十分少见,一开始就应用红黑树反而会导致所有的状况都会占用比链表大2倍的空间,事与愿违,这也是一种均衡的策略。为什么链表长度达到8?因为达到8个元素的时候,概率曾经很低了。此时树化,性价比会很高。既不会因为链表太长(8)导致复杂度加大,也不会因为概率太高导致太多节点树化。为什么红黑树转链表的阈值为6?次要是因为,如果也将该阈值设置于8,那么当hash碰撞在8时,会反生链表和红黑树的不停互相激荡转换,白白浪费资源。两头有个差值7能够避免链表和树之间的频繁转换,假如一下: 如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果HashMap不停的插入,删除元素,链表个数在8左右彷徨,就会频繁的产生红黑树转链表,链表转红黑树,效率会很低下。putVal 办法次要做了这么几件事件: 当桶数组 table 为空时,resize()初始化 table查找要插入的键值对是否曾经存在,onlyIfAbsent如果为false且oldValue等于null 则笼罩旧值如果不存在,则将键值对链入链表中,如果链表长度达到了8,且数组长度小于64,那么就从新散列,如果大于64,则创立红黑树判断键值对数量是否大于阈值,大于的话则进行扩容操作 resize办法次要做了这么几件事件:当HashMap中的元素个数超过数组大小(数组长度)*loadFactor(负载因子)时,就会进行数组扩容。loadFactor的默认值(DEFAULT_LOAD_FACTOR)是0.75。也就是说,默认状况下,数组大小为16,那么当HashMap中的元素个数超过16×0.75=12(这个值就是阈值或者边界值threshold值)的时候,就把数组的大小扩大为2×16=32,即扩充一倍,而后从新计算每个元素在数组中的地位,而这是一个十分耗性能的操作,所以如果咱们曾经预知HashMap中元素的个数,那么预知元素的个数可能无效的进步HashMap的性能 计算新桶数组的容量 newCap 和新阈值 newThr依据计算出的 newCap 创立新的桶数组,桶数组 table 也是在这里进行初始化的将键值对节点从新映射到新的桶数组里。如果节点是 TreeNode 类型,则须要拆分红黑树。如果是一般节点,则节点按原程序进行分组将键值对节点从新映射到新的tab解析: HashMap在进行扩容时,应用的rehash形式十分奇妙,因为每次扩容都是翻倍,与原来的数组长度n计算的 (n-1)&hash的后果相比,只是多了一个bit位,所以节点要么就在原来的地位,要么就被调配到"原地位+旧容量"这个地位。那么多的这一位怎么判断是0还是1呢?:e.hash & oldCap原容量,而后判断等不等于0即可。等0即新位是0,不等0即新位是1。

March 1, 2023 · 1 min · jiezi

关于hashmap:模拟HashMap冲突

最近看HashMap的源码,其中雷同下标容易产生hash抵触,然而调试须要产生hash抵触,本文模仿hash抵触。hash抵触原理HashMap抵触是key首先调用hash()办法: static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}而后应用hash值和tab数组长度做与操作: (n - 1) & hash算进去的下标,如果统一就会产生抵触。 通过ASKII码获取单个字符开始想到单字符,比方a、b、c、d、e这类字符,然而如果一个一个试的话特地繁琐,想到了ASKII码: 遍历1~100的ASKII码。通过ASKII码获取单字符: for (int i = 33; i < 100; i++) { char ch = (char) i; String str = String.valueOf(ch);}通过str获取下标,HashMap默认长度为16,所以n-1为15: int index = 15 & hash(str);获取产生hash抵触的字符算出index统一的话,就放在一个列表中。不同的index放在HashMap中,残缺代码如下: Map<Integer, List<String>> param = new HashMap<>();for (int i = 33; i < 100; i++) { char ch = (char) i; String str = String.valueOf(ch); int index = 15 & hash(str); List<String> list = param.get(index); if (list == null) { list = new ArrayList<>(); } list.add(str); param.put(index,list);}param.forEach((k,v) -> System.out.println(k + " " + Arrays.toString(v.toArray())));输入后果: ...

July 6, 2022 · 3 min · jiezi

关于hashmap:闲聊-聊一聊hash

咱们相熟的hash构造,首先是一个数组元素的哈希桶,比方下图,是长度为4的哈希桶。 也就是说,当key通过hash计算后,对4进行取模,依据后果寄存这个指定地位。比方取模后值为0,那就放第一个地位。 哈希桶的每个地位,保留的是entry的对象,这个entry对象包含key、value以及entry对象。 这个速度是十分快的,工夫复杂度是O(1),所以map的性能还是不错的。 如果模数相等呢?咱们的entry对象中包含了entry对象,也就是说会通过链表,把雷同模数的entry连接起来。 然而数据量越来越大的时候,就会发现一个问题,那就是雷同的模数会越来越多,也就是说哈希表的抵触问题越来越重大,重大到影响到性能。 那怎么办呢? 罕用的办法是通过rehash扩容来解决hash抵触,哈希桶数量的数量,让entry寄存于更多的哈希桶钟。也就是下面的4变成8,变成16,这样能够间接升高抵触的概率。 看起来很完满,除了并发可能带来的死循环(参考HashMap源码解析),仍然有一个潜在的危险点。 失常rehash的时候,步骤是这样的: 创立一个新的哈希桶,数量是之前的2倍。把旧的数据从新计算存入新的哈希桶中。删除旧的哈希桶。如果数据量很大的话,那整个过程就始终梗塞在第二个步骤中。 redisredis也用了哈希桶,他是这么解决的,把每一次的拷贝,都摊派在每一个申请中。 假如原长度为4,新长度为8。 那第一次解决申请的时候,就只解决1下面的entry链表。 此时4下面的entry链表还没解决,当第二次申请的时候,就会解决。 通过分批解决,奇妙的把一次十分耗时的操作,摊派到一个个小的操作里。(然而申请 的时候,还是要思考两个哈希桶存在的状况)。 JVM分批操作,在JVM的G1收集器也有相似的手法。 redis是单线程,所以须要防止耗时的rehash的操作导致线程阻塞,而JVM回收垃圾的时候,会stop the world,也会阻塞利用,所以也要防止长时间的垃圾回收操作。 所以G1是会依据进展工夫来进行垃圾回收,有时候回收的垃圾,并不能开释现实的内存空间,那就会分屡次进行回收,最多8次,尽可能在缩小进展工夫的状况下最大化的回收垃圾。

January 24, 2022 · 1 min · jiezi

关于hashmap:泊松分布与HashMap中的阈值8和6

PPT导出为图片整顿:

December 28, 2021 · 1 min · jiezi

关于hashmap:HashMap底层原理

一、存储构造HashMap是数据结构散列表在Java中的实现版本,通过对键值进行哈希函数计算出键值对在散列表中的下标地位,能够快速访问到相应数据,工夫复杂度为O(1)。 但因散列表数组长度无限,不同键值通过哈希函数计算出的下标地位可能统一,引发哈希抵触,为了解决这个问题,通常将哈希抵触的数据组成一个链表挂到散列表桶节点上,jdk7及以前版本都是如此解决,工夫复杂度为O(n),构造如下图: jdk8中对链表构造做了优化,在肯定条件下将链表转为红黑树,晋升查问效率。红黑树是一种二叉均衡树,工夫复杂度为O(logn),构造如下图: 二、存取原理put()办法调用哈希函数hash(key)计算出键值key的哈希值hash,再用此哈希值hash和散列表长度-1 做与运算失去数组下标;依据数组下标找到指标bucket,如果bucket上没有任何元素,就依据键值对创立一个新节点Node,赋值给该bucket上;如果以后bucket上有链表,且头结点就匹配(哈希值hash相等,key值雷同或equals相等),那么就将头结点上的值value替换;若第2、3步不满足,以后bucket上的头结点是树结构结点类型TreeNode,则转为红黑树的插入操作;若第2、3、4步都不满足,则对链表做遍历操作; 1)若链表中有节点匹配,则做value替换; 2)若没有结点匹配,则在链表开端追加;(jdk7应用头插法,jdk8应用尾插法) 3)查看链表长度是否超过TREEIFY_THRESHOLD(默认大小为8),若超过则执行红黑树转换操作; 以上操作执行完之后,再查看以后键值对的数量比例是否超过了负载因子,若超出,则进行扩容。get()办法依据key计算出hash值,进一步计算失去哈希表的指标下标index;若指标bucket头结点匹配,就返回头结点的value;若指标bucket上为红黑树,则在红黑树上查找;若不是红黑树,遍历链表;若都没匹配到,返回null;三、扩容个别状况下,当键值对数量比例超过负载因子便会触发扩容。每次扩容后的容量都是之前容量的2倍。 四、线程不平安

September 16, 2021 · 1 min · jiezi

关于hashmap:深度解析HashMap底层实现架构

摘要:剖析Map接口的具体应用以及HashMap的底层是如何实现的?本文分享自华为云社区《【图文并茂】深度解析HashMap高频面试及底层实现构造!【奔跑吧!JAVA】》,原文作者:灰小猿 。 Map接口大家应该都据说过吧?它是在Java中对键值对进行存储的一种罕用形式,同样其中的HashMap我置信大家应该也不会生疏,一说到HashMap,我想略微晓得点的小伙伴应该都说是:这是存储键值对的,存储形式是数组加链表的模式。然而其中真正是如何进行存储以及它的底层架构是如何实现的,这些你有理解吗? 可能很多小伙伴该说了,我只须要晓得它怎么应用就能够了,不须要晓得它的底层实现,但其实恰恰相反,只晓得它怎么应用是齐全不够的,而且在Java开发的面试之中,HashMap底层实现的发问和考查曾经是司空见惯的了。所以明天我就来和大家剖析一下Map接口的具体应用以及HashMap的底层是如何实现的? 小伙伴们缓缓往下看,看完相对会让你播种满满的! 1,Map接口和List接口是什么关系?对于这个问题,如果非要说这两个接口之间存在怎么的关系的话,那无非就只有一个,就都是汇合。存放数据的。在其余下面,Map接口和List接口的分割其实并不大,为什么这么说? 先来看List接口,对于List接口我在之前也和大家提到过,它是继承于Collection接口的,是Collection接口的子接口,只是用于对数据的单列存储。继承关系如下图: 而Map接口是一个顶层接口,上面蕴含了很多不同的实现类,它是用于对键值对(key:value)进行存储的,继承关系如下图: 所以Map接口和List接口的关系和应用不要混同了! 2、Map有哪些罕用的实现类?下面对于Map的继承构造咱们曾经理解了,咱们也看了其中很多不同的实现类,这些类很多也是咱们比拟相熟的,比方HashMap、TreeMap以及HashTable。在面试的时候,面试官往往就还会问,Map接口下有哪些罕用的实现类以及它们的作用,那么接下来咱们就来对这几个接口进行简略的介绍和剖析一下, HashMap:下面也说了,HashMap的底层实现是数组+链表+红黑树的模式的,同时它的数组的默认初始容量是16、扩容因子为0.75,每次采纳2倍的扩容。也就是说,每当咱们数组中的存储容量达到75%的时候,就须要对数组容量进行2倍的扩容。 HashTable:HashTable接口是线程平安,然而很早之前有应用,当初简直属于一个遗留类了,在开发中不倡议应用。 ConcurrentHashMap:这是现阶段应用应用比拟多的一种线程平安的Map实现类。在1.7以前应用的是分段锁机制实现的线程平安的。然而在1.8当前应用synchronized关键字实现的线程平安。 其中对于HashMap的考查和发问在面试中是最频繁的,这也是在日常开发中最应该深刻了解和把握的。所以接下来就次要和大家详细分析一下HashMap的实现原理以及面试中的常考问题。 3、请论述HashMap的put过程?咱们晓得HaahMap应用put的形式进行数据的存储,其中有两个参数,别离是key和value,那么对于这个键值对是如何进行贮存的呢?咱们接下来进行剖析一下。 在HashMap中应用的是数组+链表的实现形式,在HashMap的下层应用数组的模式对“雷同”的key进行存储,上层对相应的key和value应用链表的模式进行链接和存储。 留神:这里所说的雷同并不一定是key的数值雷同,而是存在某种雷同的特色,具体是哪种特色骂咱们持续往下看! HashMap将将要存储的值依照key计算其对应的数组下标,如果对应的数组下标的地位上是没有元素的,那么就将存储的元素寄存下来,然而如果该地位上曾经存在元素了,那么这就须要用到咱们下面所说的链表存储了,将数据依照链表的存储程序顺次向下存储就能够了。这就是put的简略过程,存储后果如下: 然而咱们有时候存储的数据会很多,那么如果始终应用链表的模式进行数据的存储的话就或造成咱们的链表的长度十分大,这样无论在进行删除还是在进行插入操作都是非常麻烦的,因而对于这种状况应该怎么办呢? 这里就波及到了一个链表中数据存储时,进行“树化”和“链化”的一个过程,那么什么是“树化”和“链化”呢? 当咱们在对键值对进行存储的时候,如果咱们在同一个数组下标下存储的数据过多的话,就会造成咱们的链表长度过长,导致进行删除和插入操作比拟麻烦,所以在java中规定,当链表长度大于8时,咱们会对链表进行“树化”操作,将其转换成一颗红黑树(一种二叉树,右边节点的值小于根节点,左边节点的值大于根节点),这样咱们在对元素进行查找时,就相似于进行二分查找了,这样的查找效率就会大大增加。 然而当咱们进行删除操作,将其中的某些节点删除了之后,链表的长度不再大于8了,这个时候怎么办?难道就要连忙将红黑树转化为链表的模式吗?其实并不是,只有当链表的长度小于6的时候,咱们才会将红黑树从新转化为链表,这个过程就叫做“链化”。 过程图示如下: 那么为什么要在长度8的时候进行“树化”,而在长度小于6的时候才进行“链化”呢?为什么不间接在长度小于8的时候就进行“链化”? 次要起因是因为:当删除一个元素,链表长度小于8的时候间接进行“链化”,而再减少一个元素,长度又等于8的时候,又要进行“树化”,这样重复的进行“链化”和“树化”操作特地的耗费工夫,而且也比拟麻烦。所以程序就规定,只有当当链表长度大于等于8的时候才进行“树化”,而长度小于6的时候才进行“链化”,其中对于8树化、6链化这两个阈值心愿大家牢记! 4、链表中是依照怎么的程序存放数据的?咱们当初曾经晓得了HashMap中的元素是如何寄存的,然而有时候面试官可能还会问咱们,在HashMap中,向链表中存储元素是在头结点存储的还是在尾节点存储的? 这个咱们须要晓得,对于HashMap中链表元素的存储。 在JDK1.7以及前是在头结点插入的,在JDK1.8之后是在尾节点插入的。 5、Hash(key)办法是如何实现的?咱们当初曾经晓得了HashMap中的元素是如何存储的了,那么当初就是如何应该依据key值进行相应的数组下标的计算呢? 咱们晓得HashMap的初始容量是16位,那么对于初始的16个数据位,如果将数据依照key的值进行计算存储,个别最简略的办法就是依据key值获取到一个int值,办法是: int hashCode = key.hashCode()而后对获取到的hashCode与16进行取余运算,hashCode % 16 = 0~15这样失去的永远都是0—15的下标。这也是最最原始的计算hash(key)的办法。 然而在理论状况下,这种办法计算的hash(key)并不是最优,寄存到数组中的元素并不是最扩散的,而且在计算机中进行余运算其实是十分不不便的、 所以为了计算结果尽可能的离散,当初计算数组下标最罕用的办法是:先依据key的值计算到一个hashCode,将hashCode的高18位二进制和低18位二进制进行异或运算,失去的后果再与以后数组长度减一进行与运算。最终失去一个数组下标,过程如下: int hashCode = key.hashCode()int hash = hash(key) = key.hashCode()的高16位^低16位&(n-1) 其中n是以后数组长度同时在这里要揭示一点: 在JDK1.7和JDK1.8的时候对hash(key)的计算是略有不同的 JDK1.8时,计算hash(key)进行了两次扰动 JDK1.7时,计算hash(key)进行了九次扰动,别离是四次位运算和五次异或运算 其中扰动可能了解为运算次数 以上就是Hash(key)办法的实现过程。 6、为什么HashMap的容量始终是2的倍数?HashMap的容量之所以始终是2的倍数,其实是与下面所说的hash(key)算法无关的。 起因是只有参加hash(key)的算法的(n-1)的值尽可能都是1的时候,失去的值才是离散的。如果咱们以后的数组长度是16,二进制示意是10000,n-1之后是01111,使得n-1的值尽可能都是1,对于其余是2的倍数的值减1之后失去值也是这样的。 所以只有当数组的容量长度是2的倍数的时候,计算失去的hash(key)的值才有可能是绝对离散的, 7、Hash抵触如何解决?什么是Hash抵触?就是当我计算到某一个数组下标的时候,该下标上曾经寄存元素了,这就叫Hash抵触,很显然,如果咱们计算数组下标的算法不够优良的时候,很容易将存储的数据积攒到同一个下标下面,造成过多的Hash抵触。 那么如何解决hash抵触? 最应该解决的其实就是让存储的key计算失去的数组下标尽可能的离散,也就是要求hash(key)尽可能的优化,数组长度是2的倍数。这也就是Hash抵触的次要解决办法。 具体能够查看上面HashMap要害局部的底层源码: Hash(key)的底层实现 /** * Applies a supplemental hash function to a given hashCode, which * defends against poor quality hash functions. This is critical * because HashMap uses power-of-two length hash tables, that * otherwise encounter collisions for hashCodes that do not differ * in lower bits. Note: Null keys always map to hash 0, thus index 0. */ static int hash(int h) { // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }put(key,value)办法的底层实现 /** * Associates the specified value with the specified key in this map. * If the map previously contained a mapping for the key, the old * value is replaced. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with <tt>key</tt>, or * <tt>null</tt> if there was no mapping for <tt>key</tt>. * (A <tt>null</tt> return can also indicate that the map * previously associated <tt>null</tt> with <tt>key</tt>.) */ public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }8、HashMap是如何扩容的?咱们在下面说到了HashMap的数组的初始容量是16,然而很显然16个存储位是显然不够的,那么HashMap应该如何扩容呢? ...

July 16, 2021 · 2 min · jiezi

关于hashmap:HashMap-是如何工作的图文详解一起来看看

1 HashMap 在 JAVA 中的怎么工作的?基于 Hash 的原理。 2 什么是哈希?最简略模式的 hash,是一种在对任何变量 / 对象的属性利用任何公式 / 算法后, 为其调配惟一代码的办法。 一个真正的 hash 办法必须遵循上面的准则: “ 哈希函数每次在雷同或相等的对象上利用哈希函数时, 应每次返回雷同的哈希码。换句话说, 两个相等的对象必须统一地生成雷同的哈希码。 Java 中所有的对象都有 Hash 办法,Java 中的所有对象都继承 Object 类中定义的 hashCode() 函数的默认实现。此函数通常通过将对象的外部地址转换为整数来生成哈希码,从而为所有不同的对象生成不同的哈希码。 3 HashMap 中的 Node 类Map 的定义是:将键映射到值的对象。 因而,HashMap 中必须有一些机制来存储这个键值对。答案是必定的。HashMap 有一个外部类 Node,如下所示: static class Node<K,V> implements Map.Entry<K,V> {    final int hash; // 记录hash值, 以便重hash时不须要再从新计算    final K key;     V value;    Node<K,V> next;        ...// 其余的代码}当然,Node 类具备存储为属性的键和值的映射。  key 已被标记为 final,另外还有两个字段:next 和 hash。 在上面中, 咱们将会了解这些属性的必须性。 4 键值对在 HashMap 中是如何存储的键值对在 HashMap 中是以 Node 外部类的数组寄存的, 如下所示: transient Node<K,V>[] table;哈希码计算出来之后, 会转换成该数组的下标, 在该下标中存储对应哈希码的键值对, 在此先不具体解说 hash 碰撞的状况。 该数组的长度始终是 2 的次幂, 通过以下的函数实现该过程 ...

July 16, 2021 · 2 min · jiezi

关于hashmap:面试官问我HashMap哪里不安全我支支吾吾的说了这些

前言HashMap在JDK7和JDK8是有了一些不同的,具体体现如下: JDK7HashMap底层是数组+链表,而JDK8是数组+链表+红黑树JDK7扩容采纳头插法,而JDK8采纳尾插法JDK7的rehash是全副rehash,而JDK8是局部rehash。JDK8对于key的hash值计算相比于JDK7来说有所优化。 如果还有趣味的小伙伴能够学习学习我的以下文章,写的非常具体!! 高频考题:手写HashMap JDK7、8扩容源码级详解 JDK7、8HashMap的get()、put()流程详解 JDK7 HashMapJDK7HashMap在多线程环境下会呈现死循环问题。如果此时A、B线程同时对一个HashMap进行put操作,且HashMap刚号达到扩容条件须要进行扩容 那么这两个线程都会取对HahsMap进行扩容(JDK7HashMap扩容调用 resize()办法,而resize()办法中须要调用transfer()办法将旧数组元素全副rehash到新数组中去==重点:这里在多线程环境下就会呈现问题==) void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);}void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; //对数组的每一条链表遍历rehash for (Entry<K,V> e : table) { while(null != e) { //保留下一个节点 Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } //失去对应在新数组中的索引地位 int i = indexFor(e.hash, newCapacity); //尾插法 e.next = newTable[i]; newTable[i] = e; e = next; } }}咱们假如当初有一个链表 C——>D,且C、D扩容后计算的索引地位仍然不变,那他么还在同一链表中 ...

July 12, 2021 · 2 min · jiezi

关于hashmap:HashMap底层原理附源码分析

HashMap是java中十分罕用的容器,以前只是停留在应用阶段,对于它的底层设计更是只知其一;不知其二,看到源码才晓得它奇妙的设计和工作原理。在理解底层原理之前,倡议先学习相干的数据结构几种常见数据结构,能够帮忙你更好的理解HashMap。 数据结构HashMap的数据结构是数组+链表,默认初始大小是16,默认负载因子是0.75,当理论容量大小(容量大小是蕴含每一个node的大小,不是非空数组的大小)超过定义的容量Capacity乘以负载因子的值后,HashMap会进行一次resize扩容操作,创立一个2倍大小的新数组,同时rehash所有的数据(之所以须要rehash,而不是间接复制之前的数组地位,是因为hash算法中须要应用到数组长度,扩容后数组的长度变动了) HashMap初始大小是16,设置成2的幂,便于进行位运算,另外,在手动设置HashMap大小的时候,也尽量设置成2的幂数 链表进化和进化机制:hashmap中的链表长度在大于8的时候会进化成红黑树结构,小于6的时候又会进化成链表。那么,为什么是8和6呢?那是因为在屡次的试验中发现,产生8次hash碰撞的概率十分十分小,设置成8能够在小概率事件产生后,转换数据结构,优化后续查问性能,进化阈值设置成6是为了防止设置成8或者7的时候,因为hash碰撞在8左近来回的切换数据结构. Hash算法每一个key会通过一次hash运算(先获取key的hashcode,将hashcode无符号右移16位[hashcode >>> 16],对右移后的值和hashcode进行异或运算(二进制里雷同得0,不同得1))[hash = hashcode ^ hashcode >>> 16],失去key的hash值,至于数组的index值,须要将hash值和数组长度减一后的值进行与运算(同时为1后果为1,否则为0)[(length - 1) & hash]失去。 hashmap产生hash碰撞的数组会应用尾插法(jdk7是头插法)插入一个node,node中蕴含key,hash值,value以及下一个node的指针next tips:<<左移,左边补0 >>右移,右边初始位为正补0,为负补1 >>>无符号右移,初始位补0 对于二进制来说,初始位0为正,1为负 线程平安HashMap的线程不平安,jdk7和jdk8的起因不同 在jdk7中,应用的是头插法,会有两种状况的线程不平安问题(1)在某一个数组的索引的地位,A线程要插入一对key-value,指针next指向第一个Entry,这个时候B线程也要插入一对key-value,因为A线程的赋值还未实现,所以Entry的next也指向第一个Entry,并胜利增加。随后线程A开始进行赋值操作,这个时候就会发现,在线程A执行完之后,线程B所增加的数据被笼罩了(2)有一个链表有两个相邻的Entry,Entry1和Entry2,Entry1的next指针指向Entry2,当线程A在执行resize操作的时候,Entry1还没有胜利执行完搁置到新数组并更新next的操作的时候,切换到B线程,B线程操作实现了整个HashMap的resize,此时扩容后的HashMap,因为头插法的存在,Entry2的next指针指向Entry1,同时切换到线程A,线程A继续执行Entry1搁置到新数组中的操作,Entry1的next指针指向Entry2,又因为Entry2的next指针是指向Entry1的,所以会陷入死循环,线程A无奈跳出resize这步 在jdk8中,因为尾插法的存在,不会呈现死循环的状况,然而会呈现数据笼罩的问题。假如线程A和线程B同时在put 值,它们的key的hash值通过计算后失去的index索引值雷同,此时线程A挂起,线程B进行了更新操作,之后线程A持续进行更新操作,会笼罩后面线程B的更新,线程B put的数据就隐没了

March 31, 2021 · 1 min · jiezi

关于hashmap:Java集合什么是Map

Map的原理1.Map 是什么Map用于保留具备映射关系的数据,Map汇合里保留着两组值,一组用于保留Map的Key,另一组保留着Map的value。 Map有哪些办法 2.HsahMap 是什么HashMap 是一个采纳哈希表实现的键值对汇合,继承自 AbstractMap,实现了 Map 接口。 HashMap 的非凡存储构造使得在获取指定元素前须要通过哈希运算,失去指标元素在哈希表中的地位,而后再进行大量比拟即可失去元素,这使得 HashMap 的查找效率高。 当产生 哈希抵触(碰撞)的时候,HashMap 采纳 拉链法 进行解决,因而 HashMap 的底层实现是 数组+链表,如下图: HashMap实现概述容许key/value为null,然而key只能有一个null非线程平安,多个线程同时操作同一个HashMap实例所做的批改在线程间不同步遍历时不保障任何程序,跟元素插入程序或者拜访程序无关进行遍历时如果执行HashMap的remove(Object key)或者put(Object value)办法时会疾速失败,抛出异样ConcurrentModificationException。遍历时删除元素只能通过Iterator自身的remove()办法实现HashMap中有两个重要概念因为 HashMap 扩容开销很大(须要创立新数组、从新哈希、调配等等),因而产生扩容相干的两个因素: 容量(capacity)HashMap以后长度负载因子(load factor)负载因子,默认值0.75f不利因素:容量太小扩容rehash,导致性能升高,加载因子太大则会发生冲突,查找的效率变低. 容量即该HashMap可能存储的最大的key个数,为便于扩容,容量都是2^n次方。负载因子用于计算执行扩容的阈值,默认的负载因子是0.75,该值是综合思考HashMap的get/put等多种操作时的工夫空间上的均衡,举荐应用默认值即可。如果容量时256,负载因子是0.75时,当key的总量超过192个时会进行扩容,即容量变成原来的两倍512,原有的存储的key会从新通过hash运算重新分配。常量// 如果不指定初值的话,列表的长度就为16,默认加载因子为0.75,static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16static final int MAXIMUM_CAPACITY = 1 << 30;static final float DEFAULT_LOAD_FACTOR = 0.75f;static final int TREEIFY_THRESHOLD = 8;static final int UNTREEIFY_THRESHOLD = 6;static final int MIN_TREEIFY_CAPACITY = 64;成员变量// node列表transient Node<K,V>[] table;// 缓存节点transient Set<Map.Entry<K,V>> entrySet;// 节点数量transient int size;// 批改的次数transient int modCount;// 极限值,如果节点数大于这个值就须要扩容,计算形式是capacity * loadfactorint threshold;// 加载因子,节点数值大于以后总数的肯定百分比时扩容final float loadFactor;HashMap 的实现形式/* ---------------- Public operations -------------- *//** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and load factor. * * @param initialCapacity the initial capacity * @param loadFactor the load factor * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive */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);}/** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and the default load factor (0.75). * * @param initialCapacity the initial capacity. * @throws IllegalArgumentException if the initial capacity is negative. */public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR);}/** * Constructs an empty <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false);}// Map中生成一个新的Map,属于深拷贝final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { if (table == null) { // pre-size float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) threshold = tableSizeFor(t); } else if (s > threshold) resize(); for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } }}get办法public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value;}/** * Implements Map.get and related methods. * * @param hash hash for key * @param key the key * @return the node, or null if none */final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; // 列表是否为空 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // 第一个是否要找的,判断条件统一且key统一 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); // 依据哈希抵触解决办法,每个数据项都是一个链表,须要遍历这个链表去找到咱们须要的key那项 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null;}put办法put操作时,HashMap会先进行初始化,如果没有先进行初始化操作,初始化过程会取比用户指定容量大的最近2的幂次数作为数组的初始容量,如果设置扩容的阀值也进行更新,初始化实现当前持续put办法。 ...

February 18, 2021 · 5 min · jiezi

关于hashmap:面试再问-HashMap求你把这篇文章发给他

HashMap是面试中常常问到的一个知识点,也是判断一个候选人根底是否扎实的规范之一,因为通过HashMap能够引出很多知识点,比方数据结构(数组、链表、红黑树)、equals和hashcode办法,除此之外还能够引出线程平安的问题,HashMap是我在初学阶段学到的设计的最为奇妙的汇合,外面有很多细节以及优化技巧都值得咱们深刻学习,本文将会波及到以下问题: 默认大小、负载因子以及扩容倍数底层数据结构如何解决hash抵触如何计算key的hash值数组长度为什么是2的幂次方查找、插入、扩容过程fail-fast机制如果下面的都能答复进去的话那么这篇文章可能不太适宜你,话不多说进入注释。留神:本文源码都是以JDK1.8版本解说 数据结构 在 JDK1.8 中,HashMap 是由 数组+链表+红黑树形成(1.7版本是数组+链表)当一个值中要存储到HashMap中的时候会依据Key的值来计算出他的hash,通过hash值来确认寄存到数组中的地位,如果产生hash抵触就以链表的模式存储,当链表过长的话,HashMap会把这个链表转换成红黑树来存储,如图所示:在看源码之前咱们须要先看看一些根本属性//默认初始容量为16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//默认负载因子为0.75static final float DEFAULT_LOAD_FACTOR = 0.75f;//Hash数组(在resize()中初始化)transient Node<K,V>[] table;//元素个数transient int size;//容量阈值(元素个数大于等于该值时会主动扩容)int threshold; table数组外面寄存的是Node对象,Node是HashMap的一个外部类,用来示意一个key-value,源码如下: static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value);//^示意雷同返回0,不同返回1 //Objects.hashCode(o)————>return o != null ? o.hashCode() : 0; } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; //Objects.equals(1,b)————> return (a == b) || (a != null && a.equals(b)); if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; }} 总结 ...

December 16, 2020 · 8 min · jiezi

关于hashmap:关于HashMap这篇文章已经总结很详细了

HashMap的底层数据结构?HashMap 是咱们十分罕用的数据结构,由数组和链表组合形成的数据结构。数组里每个中央都存了Key-Value这样的实例,在Java7叫Entry,在Java8中叫Node。初始化后所有的地位都为null,在put插入的时候会依据key的hash去计算一个index值。 链表?为啥须要链表?链表具体是什么样的?数组的长度是无限的,在无限的长度外面应用哈希,哈希本事就存在肯定的概率性,当两个key的hash一样时就会hash到一个值上,造成链表。 每个节点都会有本身的hash、key、value、以及下个节点,Node的源码: static class Node<K,V> implements Map.Entry<K,V>{    final int hash;    fianl K key;    V value;    Node<K,V> next;   ...}链表,新的Entry节点在插入链表的时候,是怎么插入的?Java8之前是头插法,就是说新来的值会取代原有的值,原有的值就顺推到链表中去,因为过后设计这段代码的作者认为起初的值被查找的可能性更大一点,晋升查找的效率。 Java8之后,都是采纳尾部插入。 为什么改用尾插法?改用尾插法是因为HashMap的扩容机制,数组容量是无限的,数据屡次插入,到肯定的数量就会进行扩容,也就是resize。 什么时候resize?resize的两个条件: Capacity:HashMap以后长度。LoadFactor:负载因子,默认值0.75f/** * The load factor used when none specfified in constructor */static final float FEFAULT_LOAD_FACTOR=0.75f;简略的了解就是,如果以后容量是100,当存进第76个的时候,判断发现须要进行resize了,那就进行扩容。 HashMap是怎么扩容的?分两步: 扩容:创立一个新的Entry空数组,长度为原数组的2倍。Rehash:遍历原Entry数组,把所有的Entry从新Hash到新数组。为什么不间接复制,为什么须要从新Hash?因为长度扩充后,Hash的规定也随之扭转。 Hash公式:index = HashCode(Key)  & (Length-1) 扩容后Length产生了扭转,从新计算的index与扩容前显著不一样。 Java8以前用头插法,Java8之后为什么改为尾插法了呢?先举个例子吧,咱们当初往一个容量大小为2的put两个值,负载因子是0.75,咱们在put第二个的时候就会进行resize ,当初咱们要在容量为2的容器外面用不同线程插入A,B,C,如果咱们在resize之前打个断点,那意味着数据都插入了然而还没进行resize那扩容前可能是这样的,咱们能够看到链表的指向A->B->C Tip:A的下一个指针是指向B的因为resize的赋值形式,也就是应用了单链表的头插入方式,同一地位上新元素总会被放在链表的头部地位,在旧数组中同一条Entry链上的元素,通过从新计算索引地位后,有可能放到了新数组的不同地位上。 可能呈现如下状况,B的下个指针指向了A:一旦几个线程同时都调整实现,就可能呈现环形链表。如果这个时候去取值,就会呈现--Infinite Loop。 Java8之后的尾插法应用头插法会扭转链表的程序,如果应用尾插,在扩容的时候放弃链表元素原来的程序,就不会呈现链表成环的问题了。 就是说本来是A->B,在扩容后那个链表还是A->B Java7在多线程操作HashMap时可能引起死循环,起因是扩容转移后前后链表程序倒置,在转移过程中批改了原来链表中节点的援用关系。Java8在同样的前提下并不会引起死循环,起因是扩容转移后前后链表程序不变,放弃之前节点的援用关系。 那是不是意味着Java8就能够把HashMap用在多线程中?Java8中HashMap即便不会呈现死循环,然而通过源码看到put/get办法都没有加同步锁,多线程状况下最容易呈现的就是:无奈保障上一秒put的值下一秒get的时候还是原值,所以线程平安还是无奈保障。 HashMap的默认初始化长度是多少?源码显示初始化大小是16 /** * The default initial capacity -MUST be a power of two . */static final int DEFAULT_INITIAL_CAPACITY = 1<<4;  //aka 16这样是为了位运算的不便,位与运算比算数计算的效率高了很多,之所以抉择16,是为了服务将Key映射到index的算法。咱们是通过key的HashCode值去做位运算, ...

December 10, 2020 · 1 min · jiezi

关于hashmap:来看看面试必问的HashMap一次彻底帮你搞定HashMap源码

HashMap构造数组+链表+红黑树链表大于8转红黑树,红黑树节点数小于6退回链表。 寄存的key-value的Node节点static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; }树形构造的Node节点 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; }他的继承构造是这样,能够看到继承了Node节点 看懂一条语句 hash & tab.length-1代码中多处都能够看到这条代码,实际上这条语句只是做了一个取余(%)的动作。一个&怎么做的取余的操作:HashMap的容量为2^n其二进制构造如下 任何数&2^n-1(01111…)其后果都是去0xxxx,做了疾速取余的操作。后续会看到该条语句频繁呈现 几个外围参数Node<K,V>[] table:寄存Node的数组int size:示意以后HashMap蕴含的键值对数量int threshold:示意以后HashMap可能接受的最多的键值对数量,一旦超过这个数量HashMap就会进行扩容final float loadFactor:负载因子,用于扩容int DEFAULT_INITIAL_CAPACITY = 16:默认的table初始容量float DEFAULT_LOAD_FACTOR = 0.75f:默认的负载因子int TREEIFY_THRESHOLD = 8: 链表长度大于该参数转红黑树int UNTREEIFY_THRESHOLD = 6: 当树的节点数小于等于该参数转成链表初始化办法指定了具体的容量,以及负载因子的初始化办法。当晓得须要放入的元素的个数时能够先指定防止屡次扩容造成性能节约。 ...

November 12, 2020 · 4 min · jiezi

关于hashmap:HashMap-基本存储逻辑

HashMap 是基于hashing的原理,咱们能够通过put()和get()办法贮存和获取对象。当咱们将键值对传递给put()办法时,它计算并取得键的hash值(并进一步获取索引地位),获取Map数组中的索引地位,判断是否该地位是否存在元素,1、不存在则会从新创立一个,2、否则的话判断以后节点的key是否与存入雷同则获取该node用于替换value3、该节点的key不同,则遍历: 如果是该节点的不是链表而是红黑树则遍历该树(当链表的值超过8则会转红黑树) 如果是该节点的是链表 if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//以后数组总长度-1和hash值与运算计算索引地位if ((p = tab[i = (n - 1) & hash]) == null)//从新创立一个节点,最初一个参数null,指该节点仅有一个存储对象else {Node<K,V> e; K k;//否则的话判断以后节点的key是否与存入雷同if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; }}//如果存在雷同的key则替换if (e != null) { // existing mapping for keyV oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue;}}jdk 1.8 退出红黑树村粗构造,即 当数组节点对应的链表长度大于8时,则主动转换成红黑出存储,当链表的值小于6则会从红黑树转回链表 以晋升效率 ...

November 10, 2020 · 1 min · jiezi

关于hashmap:java-hashmap

## 手写Java HashMap外围源码 上一章手写LinkedList外围源码,本章咱们来手写Java HashMap的外围源码。 咱们来先理解一下HashMap的原理。HashMap 字面意思 hash + map,map是映射的意思,HashMap就是用hash进行映射的意思。不明确?没关系。咱们来具体解说一下HashMap的原理。 HashMap 应用剖析//1 存HashMap<String,String> map = new HashMap<>();map.put("name","tom");//2 取System.out.println(map.get("name"));//输入 tom应用就是这么简略。 HashMap 原理剖析咱们晓得,Object类有一个hashCode()办法,返回对象的hashCode值,能够了解为返回了对象的内存地址,暂且不论返回的是内存地址或者其它什么也好,先不论,至于hashCode()办法回返的数是怎么算的?咱们也不论 第1 咱们只须要记住:这个函数返回的是一个数就行了。第2 HashMap外部是用了一个数组来存放数据 1 HashMap是如何把 name,tom寄存的?上面咱们用一张图来演示 从上图能够看出:注:上图中数组的大小是7,是多少都行,只是咱们这里就画了7个元素,咱们就以数组大小为7来阐明HashMap的原理。 数组的大小是7,那么数组的索引范畴是[0 , 6]获得key也就是"name"的hashCode,这是一个数,不论这个数是多少,对7进行取余数,那么范畴必定是 [0 , 6],正好和数组的索引是一样的。"name".hashCode() % 7 的值如果为2 ,那么value也就是"tom"应该寄存的地位就是2data[2] = "tom" ,存到数组中。是不是很奇妙。2 上面再来看看如何取?也用一张图来演示底层原理,如下 由上图可知: 首先也是获取key也就是"name"的hashCode值用hashCode值对数组的大小 7 进行取余数,和存的时候运行一样,必定也是2从数组的第 2 个地位把value取出,即: String value = data[2]注:有几点须要留神 某个对象的hashCode()办法返回的值,在任何时候调用,返回的值都是一样的对一个数 n 取余数 ,范畴是 [ 0, n - 1 ]注:有几个问题须要解决 存的时候,如果不同的key的hashCode对数组取余数,都正好雷同了,也就是都映射在了数组的同一地位,怎么办?这就是hash抵触问题比方 9 % 7 == 2 , 16 % 7 == 2都等于2答:数组中寄存的是一个节点的数据结构,节点有next属性,如果hash抵触了,单链表进行寄存,取的时候也是一样,遍历链表 ...

November 7, 2020 · 28 min · jiezi

关于hashmap:HashMap-的底层结构

几个重要的成员变量:static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量/*** 构造函数中没有指明负载因子的时候默认为0.75*/static final float DEFAULT_LOAD_FACTOR = 0.75f;// 数化的阈值static final int TREEIFY_THRESHOLD = 8;// 进化为链表的阈值static final int UNTREEIFY_THRESHOLD = 6;/*** 可对桶进行树型化(变为红黑树)的最小表容量。*(否则,如果一个桶中的节点太多,则会调整表的大小。)* 应至多为4*TREEIFY_THRESHOLD,以防止调整大小和树调整阈值之间的抵触。*/static final int MIN_TREEIFY_CAPACITY = 64; // 链表变为红黑树时数组容量的最小值// resize 的阈值 (capacity * load factor).int threshold;HashMap是一个用于存储Key-Value键值对的汇合,每一个键值对也叫做Node(jdk 1.7 中为Entry)。 transient Node<K,V>[]table; static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; consrtuctor; Getter /setter ; hashCode; equals; ...}常应用的是两个办法:Get 和 Put。 ...

August 15, 2020 · 2 min · jiezi

转载面试阿里HashMap-这一篇就够了

前言 某日,囧辉和共事二狗决定就谁是“¥*大厦11楼11室(只有囧辉和二狗两人)HashMap 最强者”开展一番较量。画面过于血腥,成年人请在未成年人陪同下观看。 注释 二狗:天天听你这憨逼吹牛,是时候让你晓得什么叫仁慈了。 囧辉:二狗子,这屎能够乱吃,这话不能乱说哦。 二狗:先来点简略的,介绍下 HashMap 的底层数据结构吧。 囧辉:咱们当初用的都是 JDK 1.8,底层是由“数组+链表+红黑树”组成,如下图,而在 JDK 1.8 之前是由“数组+链表”组成。 二狗:为什么要改成“数组+链表+红黑树”? 囧辉:次要是为了晋升在 hash 抵触重大时(链表过长)的查找性能,应用链表的查找性能是 O(n),而应用红黑树是 O(logn)。 二狗:那在什么时候用链表?什么时候用红黑树? 囧辉:对于插入,默认状况下是应用链表节点。当同一个索引地位的节点在新增后达到9个(阈值8):如果此时数组长度大于等于 64,则会触发链表节点转红黑树节点(treeifyBin);而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容,因为此时的数据量还比拟小。 对于移除,当同一个索引地位的节点在移除后达到 6 个,并且该索引地位的节点为红黑树节点,会触发红黑树节点转链表节点(untreeify)。 二狗:为什么链表转红黑树的阈值是8? 囧辉:咱们平时在进行方案设计时,必须思考的两个很重要的因素是:工夫和空间。对于 HashMap 也是同样的情理,简略来说,阈值为8是在工夫和空间上衡量的后果(这 B 我装定了)。 红黑树节点大小约为链表节点的2倍,在节点太少时,红黑树的查找性能劣势并不显著,付出2倍空间的代价作者感觉不值得。 现实状况下,应用随机的哈希码,节点散布在 hash 桶中的频率遵循泊松散布,依照泊松散布的公式计算,链表中节点个数为8时的概率为 0.00000006(跟大乐透一等奖差不多,中大乐透?不存在的),这个概率足够低了,并且到8个节点时,红黑树的性能劣势也会开始展示进去,因而8是一个较正当的数字。 二狗:(呦呦呦,工夫和空间上衡量的后果,还装起B来了)那为什么转回链表节点是用的6而不是复用8? 囧辉:如果咱们设置节点多于8个转红黑树,少于8个就马上转链表,当节点个数在8彷徨时,就会频繁进行红黑树和链表的转换,造成性能的损耗。 二狗:(小菜鸡,懂得还不少)那 HashMap 有哪些重要属性?别离用于做什么的? 囧辉:除了用来存储咱们的节点 table 数组外,HashMap 还有以下几个重要属性:1)size:HashMap 曾经存储的节点个数;2)threshold:扩容阈值,当 HashMap 的个数达到该值,触发扩容。3)loadFactor:负载因子,扩容阈值 = 容量 * 负载因子。 二狗:threshold 除了用于寄存扩容阈值还有其余作用吗? 囧辉:在咱们新建 HashMap 对象时, threshold 还会被用来存初始化时的容量。HashMap 直到咱们第一次插入节点时,才会对 table 进行初始化,防止不必要的空间节约。 二狗:HashMap 的默认初始容量是多少?HashMap 的容量有什么限度吗? ...

July 10, 2020 · 2 min · jiezi

面试官为什么-HashMap-的加载因子是075

有很多货色之前在学的时候没怎么留神,笔者也是在重温HashMap的时候发现有很多能够去细究的问题,最终是会回归于数学的,如HashMap的加载因子为什么是0.75?本文次要对以下内容进行介绍:为什么HashMap须要加载因子?解决抵触有什么办法?为什么加载因子肯定是0.75?而不是0.8,0.6?为什么HashMap须要加载因子?HashMap的底层是哈希表,是存储键值对的构造类型,它须要通过肯定的计算才能够确定数据在哈希表中的存储地位:static final int hash(Object key) {    int h;    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);} AbstractMappublic int hashCode() {     int h = 0;     Iterator<Entry<K,V>> i = entrySet().iterator();     while (i.hasNext())         h += i.next().hashCode();     return h;}个别的数据结构,不是查问快就是插入快,HashMap就是一个插入慢、查问快的数据结构。但这种数据结构容易产生两种问题:① 如果空间利用率高,那么通过的哈希算法计算存储地位的时候,会发现很多存储地位曾经有数据了(哈希抵触);② 如果为了防止产生哈希抵触,增大数组容量,就会导致空间利用率不高。而加载因子就是示意Hash表中元素的填满水平。加载因子 = 填入表中的元素个数 散列表的长度加载因子越大,填满的元素越多,空间利用率越高,但发生冲突的机会变大了;加载因子越小,填满的元素越少,抵触产生的机会减小,但空间节约了更多了,而且还会进步扩容rehash操作的次数。抵触的机会越大,阐明须要查找的数据还须要通过另一个路径查找,这样查找的老本就越高。因而,必须在“抵触的机会”与“空间利用率”之间,寻找一种均衡与折衷。所以咱们也能晓得,影响查找效率的因素次要有这几种:散列函数是否能够将哈希表中的数据平均地散列?怎么解决抵触?哈希表的加载因子怎么抉择?本文次要对后两个问题进行介绍。解决抵触有什么办法?1. 凋谢定址法Hi = (H(key) + di) MOD m,其中i=1,2,…,k(k<=m-1)H(key)为哈希函数,m为哈希表表长,di为增量序列,i为已发生冲突的次数。其中,凋谢定址法依据步长不同能够分为3种:1.1 线性探查法(Linear Probing):di = 1,2,3,…,m-1简略地说,就是以以后抵触地位为终点,步长为1循环查找,直到找到一个空的地位,如果循环完了都占不到地位,就阐明容器曾经满了。举个栗子,就像你在饭点去街上吃饭,挨家去看是否有地位一样。1.2 平方探测法(Quadratic Probing):di = ±12, ±22,±32,…,±k2(k≤m2)绝对于线性探查法,这就相当于的步长为di = i2来循环查找,直到找到空的地位。以下面那个例子来看,当初你不是挨家去看有没有地位了,而是拿手机算去第i2家店,而后去问这家店有没有地位。1.3 伪随机探测法:di = 伪随机数序列这个就是取随机数来作为步长。还是用下面的例子,这次就是齐全按情绪去选一家店问有没有地位了。但凋谢定址法有这些毛病:这种办法建设起来的哈希表,当抵触多的时候数据容易堆集在一起,这时候对查找不敌对;删除结点的时候不能简略将结点的空间置空,否则将截断在它填入散列表之后的同义词结点查找门路。因而如果要删除结点,只能在被删结点上增加删除标记,而不能真正删除结点;如果哈希表的空间曾经满了,还须要建设一个溢出表,来存入多进去的元素。2. 再哈希法Hi = RHi(key), 其中i=1,2,…,kRHi()函数是不同于H()的哈希函数,用于同义词产生地址抵触时,计算出另一个哈希函数地址,直到不发生冲突地位。这种办法不容易产生堆集,然而会减少计算工夫。所以再哈希法的毛病是:减少了计算工夫。3. 建设一个公共溢出区假如哈希函数的值域为[0, m-1],设向量HashTable[0,…,m-1]为根本表,每个重量寄存一个记录,另外还设置了向量OverTable[0,…,v]为溢出表。根本表中存储的是关键字的记录,一旦发生冲突,不论他们哈希函数失去的哈希地址是什么,都填入溢出表。但这个办法的毛病在于:查找抵触数据的时候,须要遍历溢出表能力失去数据。4. 链地址法(拉链法)将抵触地位的元素结构成链表。在增加数据的时候,如果哈希地址与哈希表上的元素抵触,就放在这个地位的链表上。拉链法的长处:解决抵触的形式简略,且无堆集景象,非同义词绝不会发生冲突,因而均匀查找长度较短;因为拉链法中各链表上的结点空间是动静申请的,所以它更适宜造表前无奈确定表长的状况;删除结点操作易于实现,只有简略地删除链表上的相应的结点即可。拉链法的毛病:须要额定的存储空间。从HashMap的底层构造中咱们能够看到,HashMap采纳是数组+链表红黑树的组合来作为底层构造,也就是凋谢地址法+链地址法的形式来实现HashMap。为什么HashMap加载因子肯定是0.75?而不是0.8,0.6?从上文咱们晓得,HashMap的底层其实也是哈希表(散列表),而解决抵触的形式是链地址法。HashMap的初始容量大小默认是16,为了缩小抵触产生的概率,当HashMap的数组长度达到一个临界值的时候,就会触发扩容,把所有元素rehash之后再放在扩容后的容器中,这是一个相当耗时的操作。而这个临界值就是由加载因子和以后容器的容量大小来确定的:临界值 = DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR即默认状况下是16x0.75=12时,就会触发扩容操作。那么为什么抉择了0.75作为HashMap的加载因子呢?这个跟一个统计学里很重要的原理——泊松散布无关。泊松散布是统计学和概率学常见的离散概率分布,实用于形容单位工夫内随机事件产生的次数的概率分布。有趣味的读者能够看看维基百科或者阮一峰老师的这篇文章:泊松散布和指数分布:10分钟教程[1]等号的右边,P 示意概率,N示意某种函数关系,t 示意工夫,n 示意数量。等号的左边, 示意事件的频率。在HashMap的源码中有这么一段正文:* Ideally, under random hashCodes, the frequency of* nodes in bins follows a Poisson distribution* (http:en.wikipedia.orgwikiPoisson_distribution) with a* parameter of about 0.5 on average for the default resizing* threshold of 0.75, although with a large variance because of* resizing granularity. Ignoring variance, the expected* occurrences of list size k are (exp(-0.5) * pow(0.5, k) * factorial(k)). The first values are:* 0:    0.60653066* 1:    0.30326533* 2:    0.07581633* 3:    0.01263606* 4:    0.00157952* 5:    0.00015795* 6:    0.00001316* 7:    0.00000094* 8:    0.00000006* more: less than 1 in ten million在现实状况下,应用随机哈希码,在扩容阈值(加载因子)为0.75的状况下,节点呈现在频率在Hash桶(表)中遵循参数均匀为0.5的泊松散布。疏忽方差,即X = t,P(t = k),其中t = 0.5的状况,按公式:计算结果如上述的列表所示,当一个bin中的链表长度达到8个元素的时候,概率为0.00000006,简直是一个不可能事件。所以咱们能够晓得,其实常数0.5是作为参数代入泊松散布来计算的,而加载因子0.75是作为一个条件,当HashMap长度为lengthsize ≥ 0.75时就扩容,在这个条件下,抵触后的拉链长度和概率后果为:0:    0.606530661:    0.303265332:    0.075816333:    0.012636064:    0.001579525:    0.000157956:    0.000013167:    0.000000948:    0.00000006那么为什么不能够是0.8或者0.6呢?HashMap中除了哈希算法之外,有两个参数影响了性能:初始容量和加载因子。初始容量是哈希表在创立时的容量,加载因子是哈希表在其容量主动扩容之前能够达到多满的一种度量。在维基百科来形容加载因子:对于凋谢定址法,加载因子是特地重要因素,应严格限度在0.7-0.8以下。超过0.8,查表时的CPU缓存不命中(cache missing)依照指数曲线回升。因而,一些采纳凋谢定址法的hash库,如Java的零碎库限度了加载因子为0.75,超过此值将resize散列表。在设置初始容量时应该思考到映射中所需的条目数及其加载因子,以便最大限度地缩小扩容rehash操作次数,所以,个别在应用HashMap时倡议依据预估值设置初始容量,以便缩小扩容操作。抉择0.75作为默认的加载因子,齐全是工夫和空间老本上寻求的一种折衷抉择。想要理解更多Java架构技术的,能够关注我一下,我后续也会整顿更多对于架构技术这一块的知识点分享进去,外面会分享一些:Spring,MyBatis,Netty源码剖析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化,并发编程这些成为架构师必备的常识体系。还有大厂面试题,看了相对受益匪浅

July 8, 2020 · 1 min · jiezi

HashMap-源码解读

HashMap 源码解读傻瓜源码-内容简介傻瓜源码-内容简介????【职场经验】(持续更新)精编短文:如何成为值钱的Java开发-指南如何日常学习、如何书写简历、引导面试官、系统准备面试、选择offer、提高绩效、晋升TeamLeader.....????【源码解读】(持续更新) <br/>1. 源码选材:Java架构师必须掌握的所有框架和类库源码<br/>2. 内容大纲:按照“企业应用Demo”讲解执行源码:总纲“阅读指南”、第一章“源码基础”、第二章“相关Java基础”、第三章“白话讲源码”、第四章“代码解读”、第五章“设计模式”、第六章“附录-面试习题、相关JDK方法、中文注释可运行源码项目”3. 读后问题:粉丝群答疑解惑已收录:HashMap、ReentrantLock、ThreadPoolExecutor、《Spring源码解读》、《Dubbo源码解读》.....????【面试题集】(持续更新)<br/>1. 面试题选材:Java面试常问的所有面试题和必会知识点<br/>2. 内容大纲:第一部分”注意事项“、第二部分“面试题解读”(包括:”面试题“、”答案“、”答案详解“、“实际开发解说”)3. 深度/广度:面试题集中的答案和答案详解,都是对齐一般面试要求的深度和广度4. 读后问题:粉丝群答疑解惑已收录:Java基础面试题集、Java并发面试题集、JVM面试题集、数据库(Mysql)面试题集、缓存(Redis)面试题集 .....????【粉丝群】(持续更新) <br/>收录:阿里、字节跳动、京东、小米、美团、哔哩哔哩等大厂内推???? 作者介绍:Spring系源码贡献者、世界五百强互联网公司、TeamLeader、Github开源产品作者???? 作者微信:wowangle03 (企业内推联系我) 加入我的粉丝社群,阅读更多内容。从学习到面试,从面试到工作,从 coder 到 TeamLeader,每天给你答疑解惑,还能有第二份收入! 第 1 章 阅读指南本文基于 open-jdk 1.8 版本。本文按照” Demo “解读。本文建议分为两个学习阶段,掌握了第一阶段,再进行第二阶段; 第一阶段,理解章节“源码解读”前的所有内容。即掌握 IT 技能:熟悉 HashMap 原理。第二阶段,理解章节“源码解读”(包括源码解读)之后的内容。即掌握 IT 技能:精读 HashMap 源码。建议按照本文内容顺序阅读(内容前后顺序存在依赖关系)。阅读过程中,如果遇到问题,记下来,后面不远的地方肯定有解答。阅读章节“源码解读”时,建议获得中文注释源码项目配合本文,Debug 进行阅读学习。源码项目中的注释含义 “ Demo ”在源码中,会标注“ // HashMap Demo ”。在源码中的不易定位到的主线源码,会标注 “ // tofix 主线 ”。以下注释的源码,暂时不深入讲解: 在执行“ Demo ”过程中,没有被执行到的源码(由于遍历空集合、 if 判断),会标注“ /* Demo不涉及 / ”;空实现的源码,会标注 /* 空实现 /。不是核心逻辑,并且不影响源码理解,会标注” /* 非主要逻辑 / “。有被赋值的变量,但“ Demo ”运行过程中没有使用到该变量,会标注” /* 无用逻辑 / “。锁、异常处理逻辑、变量的判空校验、日志打印的源码中没有标注注释;第 2 章 HashMap 简介 HashMap 采用 key/value 存储结构,每个 key 对应唯一的 value,允许 key 为 null,非线程安全,且不保证元素存储的顺序。 ...

June 24, 2020 · 4 min · jiezi