关于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

一起阅读HashMapjdk17源码

废话不多说,直接进入主题: 首先我们从构造方法开始: public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } 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); // 初始化加载因子(默认0.75f) this.loadFactor = loadFactor; // 初始化容器大小(默认16) threshold = initialCapacity; init(); } // 可以看到jdk1.7中hashMap的init方法并没有创建hashMap的数组和Entry, // 而是移到了put方法里,后边会讲到 void init() { } 最常用的put方法: public V put(K key, V value) { // 可以看到,初始化table是在首次put时开始的 if (table == EMPTY_TABLE) { inflateTable(threshold); } // 对key为`null`的处理,进入到方法里可以看到直接将其hash置为0,并插入到了数组下标为0的位置 if (key == null) return putForNullKey(value); // 计算hash值 int hash = hash(key); // 根据hash,查找到数组对应的下标 int i = indexFor(hash, table.length); // 遍历数组第i个位置的链表 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; // 找到相同的key,并覆盖其value if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; // 在table[i]下的链表中没有找到相同的key,将entry加入到此链表 // addEntry方法后边会再看一下 addEntry(hash, key, value, i); return null; }根据put方法的流程,我们进入到inflateTable方法看一下他的初始化代码: // 容量一定为2的n次方,比如设置size=10,则容量则为大于10的且为2的n次方=16 // Find a power of 2 >= toSize int capacity = roundUpToPowerOf2(toSize); // 计算扩容临界值:capacity * loadFactor,当size>=threshold时,触发扩容 threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); // 初始化Entry数组 table = new Entry[capacity]; initHashSeedAsNeeded(capacity); addEntry添加链表节点 能进入到addEntry方法,说明根据hash值计算出的数组下标冲突,但是key不一样 ...

July 11, 2019 · 3 min · jiezi

HashMap是非线程安全的那么原因是什么呢HashMap的死锁

由于HashMap的容量是有限的,如果HashMap中的数组的容量很小,假如只有2个,那么如果要放进10个keys的话,碰撞就会非常频繁,此时一个O(1)的查找算法,就变成了链表遍历,性能变成了O(n),这是Hash表的缺陷。 为了解决这个问题,HashMap设计了一个阈值,其值为容量的0.75,当HashMap所用容量超过了阈值后,就会自动扩充其容量。 在多线程的情况下,当重新调整HashMap大小的时候,就会存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历。如果条件竞争发生了,那么就会产生死循环了。

July 4, 2019 · 1 min · jiezi

HashMap的实现原理笔记

序HashMap是Java中常用的Map接口的实现类,因为在日常工作中非常频繁的出现,所以在大部分的Java面试中都会问几个关于HashMap的问题。掌握HashMap的实现原理,已经是Java程序员的基础操作了。Map接口映射(Map)是一种用于存放键/值对的数据结构。如果提供了键,就能直接找到相对应的值。HashMap(哈希映射)是Map接口的一个实现类,主要使用哈希来实现键与值的映射。定义。映射是一种用于存放键/值对的数据结构,主要支持两种操作:插入(put),即将一组新的键值对存入映射中;查找(get),即根据给定的键得到相应的值。HashMap的底层数据结构HashMap的底层是用散列表实现的,散列表是一种用数组来存储键值对的数据结构,它使用一个散列函数将键转换成数组的一个索引然后存储值。不过会有不同的键被散列成同个索引的情况出现,这叫做碰撞冲突。HashMap用拉链法来解决这个问题,即散列表数组中的每个元素都指向一条链表,链表中的每个节点都存储了被散列到这个索引的键值对。HashMap的散列函数根据散列表的定义我们知道,想要弄清楚HashMap的实现,我们首先需要知道HashMap的散列函数是怎么实现的,即HashMap是如何将一个键映射到数组的索引的。static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}上面就是HashMap的散列函数源码了,可以看到如果键为null的话它的索引固定为0,即HashMap是支持使用null作为键的。如果键不为null就用Java对象都有的hashCode方法获得一个哈希值,并将这个哈希值的低16位与高16位做异或处理,高16位不变。这里有个问题就是为什么不直接用键的hashcode,而要将hashcode的低16位与高16位做异或处理?这里是因为有三个前提:int有32位HashMap的数组长度都是2的幂获取数组索引需要将键的哈希值和数组长度-1做一个与操作(&)得到,即tab[(n - 1) & hash]首先因为HashMap的数组长度都是2的幂,(n - 1)的高16位都是0,所以只有键的低16位参与索引运算。如果直接用键的hashcode的话,就会有很多碰撞冲突,所以用这种方法使得hashcoede的高16位也参与到索引的运算中来。下面是字符串“1234”在数组长度为16的索引运算过程:public static void main(String[] args) { int hashcode = “1234”.hashCode(); System.out.println(Integer.toBinaryString(hashcode)); // 输出为 10111 0000100001000010 System.out.println(Integer.toBinaryString(hashcode >>> 16)); // 输出为 10111 System.out.println(Integer.toBinaryString(hashcode ^ (hashcode >>> 16))); // 输出为 10111 0000100001010101 System.out.println(Integer.toBinaryString(16 - 1)); // 输出为 1111 System.out.println(Integer.toBinaryString((16 - 1) & (hashcode ^ (hashcode >>> 16)))); // 输出为 101}碰撞冲突虽然HashMap对散列函数做了很多优化,但是碰撞冲突还是不可避免的会出现。为了解决这个问题HashMap使用了拉链法,使用链表来存储碰撞冲突的键值对。并在JDK 8中进行了优化,当链表长度到达某个指定值时HashMap会自动将链表优化为红黑树。频繁碰撞冲突还可能是因为数组长度不够的原因,HashMap还会根据键值对的数量进行自动扩容。自动扩容在讲HashMap的自动扩容前,先来看看HashMap有哪些相关的属性:Node<K,V>[] table; 存放键值对的数组int size; 已存放键值对的数量int threshold; 当键值对的数量等于这个值的时候HashMap将进行扩容,值等于数组长度 * loadFactorfinal float loadFactor; 负载因子,用于计算threshold的值,默认值为0.75static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 数组长度的默认值16根据这些属性可以知道,HashMap的自动扩容会根据数组长度和负载因子的积得到一个threshold的值,当键值对的数量等于threshold时就会开始扩容,下面是扩容的源码。大概过程是新建一个长度为旧数组两倍的新数组,并将原有的键值对都重新映射到新数组上。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; 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; } } } } } return newTab; }后记这次主要是理解了一下HashMap的实现原理,特别重点写了很多关于散列函数的理解,并没有按照源码一行行的去理解。之所以这样是因为写这篇的动力主要来源于面试……而面试则只要讲下原理就可以了,并不需要把源码背下来。之前看HashMap的时候对散列函数都是跳过去的,只知道是用来计算键的hash,不知道里面的原理。其实还有链表转红黑树的地方没有弄清楚,主要是红黑树不怎么理解,基础的重要性体现了出来。我的博客 ...

April 4, 2019 · 2 min · jiezi

理解HashMap

HashMap源码分析基于JDK7的HashMap源码分析类的介绍下面的类介绍是从源码的英文翻译来的HashMap是基于哈希表实现的Map接口实现类。这个实现提供所有的map相关的操作,允许使用null的键和null的值。(HashMap与Hashtable大致是一样的,只是HashMap是不同步的,且它允许你null的键和值。);另外,HashMap内部元素排列是无序的。假设哈希函数能将元素合理地分散在各个哈希桶中,那么HashMap的put、get等基础操作的效率会很高(时间复杂度是常数级别O(n))。HashMap的迭代所有元素的时间与它的实例的容量(哈希桶的数量)及大小(键值对的数量)之和成正比。因此,如果你很在意HashMap的迭代性能,就不应该初始容量设置得很高,或者把负载因子设置得很低。一个HashMap的实例有两个参数会影响到它的性能:初始容量和负载因子。容量是指哈希表中桶的数量,初始容量就是哈希表创建时指定的初始大小。负载因子是一个度量,用来衡量当哈希表的容量满到什么程度时,哈希表就应该自动扩容。到哈希表中元素的数量超过负载因子和当前容量的乘积时,哈希表会重新计算哈希(rehashed)(即重建内部数据结构),哈希表桶的数量大约会变成原来的两倍。一般来说,默认把负载因子值设置成0.75,在时间成本和空间成本之间是比较好的权衡。该值再高一点能减少空间开销,但会增加查找成本(表现在HashMap类的大多数操作中,包括get和put)。所以我们在设置初始化容量时,应该合理考虑预期装载的元素数量以及负载因子,从而减少rehash的操作次数。如果初始容量大于最大条目数除以加载因子(initial capacity > max entries / load factor),则不会发生重新加载操作。如果HashMap的实例需要存储很多元素(键值对),创建HashMap时指定足够大的容量可以令它的存储效率比自动扩容高很多。请注意如果很多的键使用的hashCode()方法结果都相同,那么哈希表的性能会很慢。为了改善影响,当键是Comparable时,HashMap会用这些键的排序来提升效率。请注意,HashMap是不同步的。如果多条线程同时访问一个HashMap,且至少有一条线程发生了结构性改动,那么它必须在外部进行同步。(结构性改动是指任何增加或删除键值对的操作,在源码中具体体现是导致modCount属性改动的操作,仅仅修改一个键对应的值则不属于结构性改动)。外部同步通常通过同步一个封装了这个map的对象完成。如果没有这样的对象,那么可以使用Collections.synchronizedMap把一个map转换成同步的map,这个动作最好在创建的时候完成,避免在转换前意外访问到不同步的map。Map m = Collections.synchronizedMap(new HashMap(…));HashMap的迭代器所有集合相关的方法都是快速失败的(fail-fast):如果创建迭代器后,除了迭代器自身的remove方法之外,map发生了结构性改动,迭代器会抛出ConcurrentModificationException。因此,面对并发的修改,迭代吗快速、干净利落地失败,而不会冒任何风险。请注意,迭代器快速失败的特性在不同步的并发修改时,是不能作出硬性保证的。快速失败的迭代器会尽最大努力抛出ConcurrentModificationException。因此,编写依赖于此异常的程序以确保其正确性是错误的:迭代器的快速失败行为应该仅用于检测错误。构造函数HashMap的构造函数一共有四种:无参构造,初始容量默认16,负载因子默认0.75指定初始容量,负载因子默认0.75指定初始容量和负载因子通过传入的map构造其中1、2、4都会调用第3种构造函数,第4种只是用已有的Map构造一个HashMap的便捷方法,所以这里重点看3、4两种构造函数的实现。 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //…… // 空表 static final Entry<?,?>[] EMPTY_TABLE = {}; // 哈希表 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; // 容器扩容阈值,当容器大小(size)达到此值时,容器就会扩容。 // size = 容量 * 负载因子 // 如果table == EMPTY_TABLE,那么就会用这个值作为初始容量,创建新的哈希表 int threshold; // 负载因子 final float loadFactor; // 构造函数3:指定初始容量和负载因子 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; // 默认的阈值等于初始化容量 threshold = initialCapacity; init(); } // 构造函数4:用传入的map构造一个新的HashMap public HashMap(Map<? extends K, ? extends V> m) { this(Math.max((int) ( m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); // 分配哈希表空间 inflateTable(threshold); putAllForCreate(m); } //…… }上面的源码中,需要注意几点:扩容阈值默认等于初始容量,16。当哈希表为空表时,HashMap会在内部以该阈值作为初始容量建哈希表,哈希表实质是一个数组inflateTable方法就是建立哈希表,分配表内存空间的操作(inflate翻译为“膨胀”的意思,后面会详述)。但是指定初始容量和负载因子的构造方法并没有马上调用inflateTable。查找源码中全部调用inflateTable的地方有:graph LRHashMap构造函数-Map为参数 –> inflateTableput –> inflateTableputAll –> inflateTableclone –> inflateTablereadObject –> inflateTable初步看上去,只有参数列表是Map的构造函数调用了inflateTable,但HashMap(Map map)构造函数内部的逻辑是先调用一下HashMap(int initialCapacity, float loadFactor)构造函数初始化完了容量和负载因子后,再调用inflateTable的。所以小结一点:HashMap在初始化阶段不会马上创建哈希表。调用逻辑为了更好理解代码的调用,下图列出一些方法之间的调用关系:内部数据结构HashMap内部维护的数据结构是数组+链表,每个键值对都存储在HashMap的静态内部类Entry中,结构如下图:put的实现 public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); 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; } void addEntry(int hash, K key, V value, int bucketIndex) { // 如果容器大小大于等于阈值,且目标桶的entry不等于null if ((size >= threshold) && (null != table[bucketIndex])) { // 容器扩容: 哈希表原长度 * 2 resize(2 * table.length); // 重新计算键的哈希值 hash = (null != key) ? hash(key) : 0; // 重新计算哈希值对应存储的哈希表的位置 bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }在put方法内部,会先判断哈希表是不是空表,如果是空表就建立哈希表(上面提到的内部数据结构中的数组),建好表后,就有空间可以存放键值对了。要存放键值对,需要先根据key计算哈希码(hash),哈希码返回是一个int类型的数值,再根据哈希码计算出在固定长度的数组中存放的位置(下标)得到下标后,就要在哈希表中找到存储的位置。HashMap会先加载指定下标中存放的Entry对象,如果Entry不为空,就比较该Entry的hash和key(比较key的时候,用==和equals来比较)。如果跟put进来的hash、key匹配,就覆盖该Entry上的value,然后直接返回旧的value;否则,就找该Entry指向的下一个Entry,直到最后一个Entry为止。如果HashMap加载指定下标中存放的Entry对象是null,又或者是找完整条Entry链表都没有匹配的hash和key。那么就调用addEntry新增一个EntryaddEntry方法中会做一些前置处理。HashMap会判断容器当前存放的键值对数量是否达到了设定的扩容阈值,如果达到了就扩容2倍。扩容后重新计算哈希码,并根据新哈希码和新数组长度重新计算存储位置。做好潜质处理后,就调用createEntry新增一个Entry。由于上面已经做了前置的处理,createEntry方法就不用担心扩容的问题,放心存Entry即可。该方法会在给定的下标为止存放put进来的key,value,当然这个key,value是包装在Entry中的,让后将Entry指向旧的Entry。建哈希表的逻辑(inflateTable)建哈希表是在inflateTable方法中实现的: /** * 将一个数换算成2的n次幂 * @param number * @return / private static int roundUpToPowerOf2(int number) { // assert number >= 0 : “number must be non-negative”; return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1; // 理解 Integer.highestOneBit((number - 1) << 1) // 比如 number = 23,23 - 1 = 22,二进制是:10110 // 22 左移一位(右边补1个0),结果是:101100 // Integer.highestOneBit() 函数的作用是取左边最高一位,其余位取0, // 即:101100 -> 100000,换成十进制就是 32 } /* * inflate有“膨胀”、“充气”的意思。 * 理解为初始化哈希表,分配哈希表内存空间 / private void inflateTable(int toSize) { // Find a power of 2 >= toSize // 找出大于等于toSize的2的n次幂,作为哈希表的容量 int capacity = roundUpToPowerOf2(toSize); // 计算新的扩容阈值: 容量 * 负载因子 threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); // 指定容量建哈希表 table = new Entry[capacity]; // 根据容量判断是否需要初始化hashSeed initHashSeedAsNeeded(capacity); }理解一下roundUpToPowerOf2方法:roundUpToPowerOf2部分计算结果:roundUpToPowerOf2(0) = 1roundUpToPowerOf2(1) = 1roundUpToPowerOf2(2) = 2roundUpToPowerOf2(3) = 4roundUpToPowerOf2(4) = 4roundUpToPowerOf2(5) = 8roundUpToPowerOf2(6) = 8roundUpToPowerOf2(7) = 8roundUpToPowerOf2(8) = 8roundUpToPowerOf2(9) = 16roundUpToPowerOf2(10) = 16roundUpToPowerOf2(11) = 16roundUpToPowerOf2(12) = 16roundUpToPowerOf2(13) = 16roundUpToPowerOf2(14) = 16roundUpToPowerOf2(15) = 16roundUpToPowerOf2(16) = 16roundUpToPowerOf2(17) = 32roundUpToPowerOf2(6)计算示例:计算公式:Integer.highestOneBit((5 - 1) << 1)计算5<<1: 00000101<<1————- 00001010 1010的十进制是10,然后计算Integer.highestOneBit(10),该函数的作用是取传入数值的最高位然后其余低位取0,所以Integer.highestOneBit(10)应该等于二进制的1000,即8值得注意的是,inflateTable中最后还调用了一个initHashSeedAsNeeded(capacity)方法,该方法是用来依据容量决定是否需要初始化hashSeed,hashSeed默认是0,如果初始化hashSeed,它的值将会是一个随机值。Alternative hashing与hashSeed在源码中有一个常量ALTERNATIVE_HASHING_THRESHOLD_DEFAULT,它的注释提供了一些值得注意的信息: /* * The default threshold of map capacity above which alternative hashing is * used for String keys. Alternative hashing reduces the incidence of * collisions due to weak hash code calculation for String keys. * <p/> * This value may be overridden by defining the system property * {@code jdk.map.althashing.threshold}. A property value of {@code 1} * forces alternative hashing to be used at all times whereas * {@code -1} value ensures that alternative hashing is never used. / static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;大意是说,ALTERNATIVE_HASHING_THRESHOLD_DEFAULT是一个默认的阈值,当一个键值对的键是String类型时,且map的容量达到了这个阈值,就启用备用哈希(alternative hashing)。备用哈希可以减少String类型的key计算哈希码(更容易)发生哈希碰撞的发生率。该值可以通过定义系统属性jdk.map.althashing.threshold来指定。如果该值是1,表示强制总是使用备用哈希;如果是-1则表示禁用。HashMap有一个静态内部类Holder,它的作用是在虚拟机启动后根据jdk.map.althashing.threshold和ALTERNATIVE_HASHING_THRESHOLD_DEFAULT初始化ALTERNATIVE_HASHING_THRESHOLD,相关代码如下: /* * Holder维护着一些只有在虚拟机启动后才能初始化的值 / private static class Holder { /* * 触发启用备用哈希的哈希表容量阈值 / static final int ALTERNATIVE_HASHING_THRESHOLD; static { // 读取JVM参数 -Djdk.map.althashing.threshold String altThreshold = java.security.AccessController.doPrivileged( new sun.security.action.GetPropertyAction( “jdk.map.althashing.threshold”)); int threshold; try { // 如果该参数没有值,采用默认值 threshold = (null != altThreshold) ? Integer.parseInt(altThreshold) : ALTERNATIVE_HASHING_THRESHOLD_DEFAULT; // 如果参数值为-1,禁用备用哈希 // ALTERNATIVE_HASHING_THRESHOLD_DEFAULT也是等于Integer.MAX_VALUE // 所以jdk默认是禁用备用哈希的 if (threshold == -1) { threshold = Integer.MAX_VALUE; } // 参数为其它负数,则视为非法参数 if (threshold < 0) { throw new IllegalArgumentException(“value must be positive integer.”); } } catch(IllegalArgumentException failed) { throw new Error(“Illegal value for ‘jdk.map.althashing.threshold’”, failed); } ALTERNATIVE_HASHING_THRESHOLD = threshold; } } 之前提到过,inflateTable中最后还调用了一个initHashSeedAsNeeded(capacity)方法,该方法是用来依据容量决定是否需要初始化hashSeed,hashSeed默认是0,如果初始化hashSeed。所以下面来看看这个方法: /* * A randomizing value associated with this instance that is applied to * hash code of keys to make hash collisions harder to find. If 0 then * alternative hashing is disabled. / transient int hashSeed = 0; /* * 按需初始化哈希种子 / final boolean initHashSeedAsNeeded(int capacity) { // 如果hashSeed != 0,表示当前正在使用备用哈希 boolean currentAltHashing = hashSeed != 0; // 如果vm启动了且map的容量大于阈值,使用备用哈希 boolean useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); // 异或操作,如果两值同时为false,或同时为true,都算是false。 boolean switching = currentAltHashing ^ useAltHashing; if (switching) { // 把hashSeed设置成随机值 hashSeed = useAltHashing ? sun.misc.Hashing.randomHashSeed(this) : 0; } return switching; }从hashSeed变量的注释可以看出,哈希种子一个随机值,在计算key的哈希码时会用到这个种子,目的是为了进一步减少哈希碰撞。如果hashSeed=0表示禁用备用哈希。而Holder中维护的ALTERNATIVE_HASHING_THRESHOLD是触发启用备用哈希的阈值,该值表示,如果容器的容量(注意是容量,不是实际大小)达到了该值,容器应该启用备用哈希。Holder会尝试读取JVM启动时传入的参数-Djdk.map.althashing.threshold并赋值给ALTERNATIVE_HASHING_THRESHOLD。它的值有如下含义:ALTERNATIVE_HASHING_THRESHOLD = 1,总是使用备用哈希ALTERNATIVE_HASHING_THRESHOLD = -1,禁用备用哈希在initHashSeedAsNeeded(int capacity)方法中,会判断如果容器的容量>=ALTERNATIVE_HASHING_THRESHOLD,就会生成一个随机的哈希种子hashSeed,该种子会在put方法调用过程中的hash方法中使用到: /* * 获取key的哈希码,并应用一个补充的哈希函数,构成最终的哈希码。 * 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. / final int hash(Object k) { // 如果哈希种子是随机值,使用备用哈希 // (方法调用链:inflateTable()–>initHashSeedAsNeeded()–>hash(), // 在initHashSeedAsNeeded()中已判断了是否需要初始化哈希种子) int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); // 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); }计算存储下标(indexFor) /* * 根据哈希码计算返回哈希表的下标 / static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : “length must be a non-zero power of 2”; return h & (length-1); }这段代码和简单,却有几个有意思的地方。为什么容量要设计成2的n次幂注意,容量实质就是内部数组的length,还要注意是2的n次幂,不是2的倍数。先看下面的测试代码:public class Main { static final int hash(Object k) { int hashSeed = 0; int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } static int indexFor(int h, int length) { return h & (length-1); } public static void main(String[] args) { String key = “14587”; int h = hash(key); int capacity = 16; for (int i = 0; i < 10; i++) { System.out.println(String.format(“哈希码: %d, 容量: %d, 下标: %d”, h, // 同一个哈希码 (capacity<<i), // 不同的容量 indexFor(h,capacity<<i))); //计算出来的下标 } }// key: hello// 哈希码: 96207088, 容量: 16, 下标: 0// 哈希码: 96207088, 容量: 32, 下标: 16// 哈希码: 96207088, 容量: 64, 下标: 48// 哈希码: 96207088, 容量: 128, 下标: 112// 哈希码: 96207088, 容量: 256, 下标: 240// 哈希码: 96207088, 容量: 512, 下标: 240// 哈希码: 96207088, 容量: 1024, 下标: 240// 哈希码: 96207088, 容量: 2048, 下标: 240// 哈希码: 96207088, 容量: 4096, 下标: 240// 哈希码: 96207088, 容量: 8192, 下标: 240// key: 4// 哈希码: 55, 容量: 16, 下标: 7// 哈希码: 55, 容量: 32, 下标: 23// 哈希码: 55, 容量: 64, 下标: 55// 哈希码: 55, 容量: 128, 下标: 55// 哈希码: 55, 容量: 256, 下标: 55// 哈希码: 55, 容量: 512, 下标: 55// 哈希码: 55, 容量: 1024, 下标: 55// 哈希码: 55, 容量: 2048, 下标: 55// 哈希码: 55, 容量: 4096, 下标: 55// 哈希码: 55, 容量: 8192, 下标: 55// key: 14587// 哈希码: 48489485, 容量: 16, 下标: 13// 哈希码: 48489485, 容量: 32, 下标: 13// 哈希码: 48489485, 容量: 64, 下标: 13// 哈希码: 48489485, 容量: 128, 下标: 13// 哈希码: 48489485, 容量: 256, 下标: 13// 哈希码: 48489485, 容量: 512, 下标: 13// 哈希码: 48489485, 容量: 1024, 下标: 13// 哈希码: 48489485, 容量: 2048, 下标: 1037// 哈希码: 48489485, 容量: 4096, 下标: 1037// 哈希码: 48489485, 容量: 8192, 下标: 1037}上面的hash、indexFor都是从HashMap源码中拷过来的,hashSeed=0也是HashMap默认的值,main方法中按key计算哈希码再按哈希码和数组长度计算下标也是put方法中的执行逻辑。从测试结果可以看出,相同的哈希码,在多次扩容时,使用indexFor的算法,下标变动较少,这样能减少扩容引起的移动Entry的操作次数。可以看看key为4,容量为16、32、64……时indexFor计算下标的过程。字符串“4”的哈希码是:55(二进制110111)当length = 16时: h & (length-1)= 55 & (16-1)= 110111 & 1111当length = 32时: h & (32-1)= 55 & (16-1)= 110111 & 11111当length = 64时: h & (length-1)= 55 & (64-1)= 110111 & 111111由于容量每次扩容都会翻倍(容量 x 2),翻到特定次数后(红色虚线往左),跟h做与运算的位肯定是全部都是1,所以算出来的下标都会是一样的。这样子,虽然扩容会引起下标变动,但相对稳定。试想想,如果容量是17、33、65…..那么lenght-1的二进制除了高位(最左一位)是1,其余是0,不同hash和length-1做与运算算出来的下标就更容易有重复的下标。使lenght-1的全部位为1,能使计算出来的下标分布更均匀,减少哈希碰撞。小结一下,容量设计成2的n次幂是为了:在put方法中,有调用indexFor计算下标,容量设计成2的n次幂能使下标相对均匀,减少哈希碰撞在扩容相关的transfer方法中,也有调用indexFor重新计算下标。容量设计成2的n次幂能使扩容时重新计算的下标相对稳定,减少移动元素扩容与线程安全问题 /* * Rehashes the contents of this map into a new array with a * larger capacity. This method is called automatically when the * number of keys in this map reaches its threshold. * * If current capacity is MAXIMUM_CAPACITY, this method does not * resize the map, but sets threshold to Integer.MAX_VALUE. * This has the effect of preventing future calls. * * @param newCapacity the new capacity, MUST be a power of two; * must be greater than current capacity unless current * capacity is MAXIMUM_CAPACITY (in which case value * is irrelevant). / 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; 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; } } }从上图显示的转移过程可以看出,链表在转移后会逆序。3–>7–>9 变成 9–>7–>3。在单线程环境下,是不会出现闭合的回路的。但是在多线程环境下,有可能多条线程都调用transfer,而transfer方法中访问了一个全局变量table,并修改下标中指向的Entry。由于转移过程会导致链表逆序,就有可能出现闭环的引用:3–>7–>9–>3,然后,在调用get方法的时候,就出现死循环。 ...

March 15, 2019 · 7 min · jiezi

面试题·HashMap和Hashtable的区别(转载再整理)

原文链接: Javarevisited 翻译: ImportNew.com - 唐小娟译文链接: http://www.importnew.com/7010...HashMap和Hashtable的比较是Java面试中的常见问题,用来考验程序员是否能够正确使用集合类以及是否可以随机应变使用多种思路解决问题。HashMap的工作原理、ArrayList与Vector的比较以及这个问题是有关Java 集合框架的最经典的问题。Hashtable是个过时的集合类,存在于Java API中很久了。在Java 4中被重写了,实现了Map接口,所以自此以后也成了Java集合框架中的一部分。Hashtable和HashMap在Java面试中相当容易被问到,甚至成为了集合框架面试题中最常被考的问题,所以在参加任何Java面试之前,都不要忘了准备这一题。这篇文章中,我们不仅将会看到HashMap和Hashtable的区别,还将看到它们之间的相似之处。HashMap和Hashtable的区别HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别。主要的区别有:线程安全性,同步(synchronization),以及速度。HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。HashMap不能保证随着时间的推移Map中的元素次序是不变的。要注意的一些重要术语:sychronized意味着在一次仅有一个线程能够更改Hashtable。就是说任何线程要更新Hashtable时要首先获得同步锁,其它线程要等到同步锁被释放之后才能再次获得同步锁更新Hashtable。Fail-safe和iterator迭代器相关。如果某个集合对象创建了Iterator或者ListIterator,然后其它的线程试图“结构上”更改集合对象,将会抛出ConcurrentModificationException异常。但其它线程可以通过set()方法更改集合对象是允许的,因为这并没有从“结构上”更改集合。但是假如已经从结构上进行了更改,再调用set()方法,将会抛出IllegalArgumentException异常。结构上的更改指的是删除或者插入一个元素,这样会影响到map的结构。我们能否让HashMap同步?HashMap可以通过下面的语句进行同步:Map m = Collections.synchronizeMap(hashMap);结论Hashtable和HashMap有几个主要的不同:线程安全以及速度。仅在你需要完全的线程安全的时候使用Hashtable,Hashtable是java 4时代的过时产物,ConcurrentHashMap是它的替代品。而如果你使用Java 5或以上的话,请使用ConcurrentHashMap吧。

February 25, 2019 · 1 min · jiezi

HashMap剖析之put()和get()方法

前言本文是基于Java 8的HashMap进行分析,主要是分析HashMap中的put()和get()方法。下面将会分析这部分的源码,如果觉得源码分析内容太啰嗦,可以跳过源码部分,直接看源码下面的总结。put()方法源码分析HashMap的put()方法是我们最常用的方法,但是put()方法是怎么工作的呢?put()方法 /** * HashMap的put()方法支持key/value为null / public V put(K key, V value) { //实际上是先调用HashMap的hash()方法获取到key的hash值 //然后调用HashMap的putVal()方法 return putVal(hash(key), key, value, false, true); }put()方法实际上是调用hash()方法获取到key的hash值调用putVal()方法存储key-value核心方法是putVal()方法,下面我会先分析一下hash()方法,因为这个方法涉及到hash值这个关键属性的计算。hash()方法static final int hash(Object key) { int h; // key为null时,hash值为0 // key不为null时,调用key对象的hashCode()方法并通过位运算异或和无符号右移将高位分散到低位 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }hash()方法指定了null的hash值为0。这样就可以支持key为null。(h = key.hashCode()) ^ (h >>> 16)这段代码通过位运算异或和无符号右移将高位分散到低位,这样做可以减少哈希碰撞的概率(这块不是很清楚原理,是从方法注释上了解到的)putVal()方法 /* * Map.put()方法的实际实现 * * @param hash key的hash值 * @param key 键值对中的key * @param value 键值对中的value * @param onlyIfAbsent 如果为true,则键值对中的值已经存在则不修改这个值 * @param evict 如果为false,则是处于创建模式 * @return 上一次的value,如果上一次的value不存在,则为null / final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //tab用于暂存散列表table。p为散列表中对应索引的链表的头节点的指针。n存储tab的长度。i则为命中的散列表的索引 Node<K,V>[] tab; Node<K,V> p; int n, i; //给tab和n赋值 //当tab为null或者tab的长度n为0时,触发resize()来初始化tab if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //使用(n - 1) & hash(等价于hash%n)计算命中的散列表索引,同时判断散列表对应索引的链表是否存在 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值和key一致。需要注意,null的hash值为0 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 对应的键值对已经存在,记录下来 e = p; else if (p instanceof TreeNode)//判断对应的链表是否转化为红黑树 //若是,则直接调用红黑树的putTreeVal()方法 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开始,所以为阈值-1 // 将链表转化为红黑树 treeifyBin(tab, hash); // 中断循环 break; } // 判断当前遍历的节点的hash值和key是否与入参的hash值和key一致,即key是否已经存在 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // key已经存在,中断循环 break; // 记录当前遍历的节点 p = e; } } if (e != null) { // Map中存在重复的key V oldValue = e.value;//记录下旧值 if (!onlyIfAbsent || oldValue == null)//判断值存在是否可以进行修改以及旧值是否为null e.value = value;//修改该节点的值 afterNodeAccess(e);// 链表节点的回调方法,此处为空方法 return oldValue;//返回旧值 } } // HashMap发生结构变化,变化次数累加 ++modCount; // 键值对个数自增,同时判断是否达到扩容的阈值 if (++size > threshold) resize(); // 链表节点的回调方法,此处为空方法 afterNodeInsertion(evict); // 此处返回null是因为链表新增了节点,所以上一次的值必然为null return null; }putVal()方法的关键点:若table没有初始化则调用reszie()方法初始化。计算命中的散列表索引位置,公式为(n - 1) & hash(等价于hash%n)。其中n为散列表长度,hash为插入的键值对的key的哈希值。判断散列表对应索引中的首节点是否为null,若为null,则创建链表,否则进入下一步。判断该首节点是否与插入的键值对的key和hash一致,若一致则替换该节点的值为value,否则进入下一步判断首节点是否为树节点,若是则调用树节点的putTreeVal()方法遍历红黑树,否则遍历链表。遍历红黑树时,若存在key和hash相同的节点就替换对应节点的值value,若不存在则插入新的树节点。遍历链表时,若存在key和hash相同的节点就替换对应节点的值为value。若找不到key和hash相同的节点,则链表尾部插入节点,同时进入下一步。若当前链表长度大于或等于树化阈值TREEIFY_THRESHOLD(8)时,则将链表转化为红黑树。get()方法源码分析除了HashMap的put()方法外,get()方法也是一个我们常用的方法,下面开始分析其关键的源码。get()方法/* * 返回key对应的value,如果不存在则返回null /public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }get()方法实际上是调用hash()方法获取到key的hash值调用getNode()方法通过key和hash获取对应的value,不存在则返回null。核心方法是getNode()方法,下面我会先分析一下getNode()方法。getNode()方法 /* * Map.get()方法的实际实现 * @param hash key的哈希值 * @param key 查询用的key * @return 节点或者是节点不存在是返回null */ final Node<K,V> getNode(int hash, Object key) { //tab用于暂存散列表table。first为散列表中对应索引的链表的头节点的指针。n存储tab的长度。i则为命中的散列表的索引 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) { if (first.hash == hash && ((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);//改为遍历红黑树 do {//遍历链表是否有符合的节点 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } //不存在对应的key,返回null return null; }getNode()方法的关键点:若散列表table不为null且长度大于0且其索引为(n - 1) & hash(等价于hash%n)的节点不为null。其中n为散列表长度,hash为插入的键值对的key的哈希值。则进入下一步,否则直接返回null判断首节点的key和hash是否与入参一致,若相同则返回首节点,否则进入下一步。判断节点个数只有1个,若是则返回null,否则进入下一步判断首节点是否为树节点,若是则遍历红黑树,否则为链表,进入下一步遍历链表,检索key和hash与入参相同的节点,若找到则返回该节点,否则返回null总结put()和get()方法是HashMap的常用方法,通过学习其源码了解到HashMap是如何使用拉链法解决哈希冲突。而下面将会通过两幅图展示put()和get()的执行过程:put()方法图解get()方法图解 ...

February 14, 2019 · 3 min · jiezi

HashMap剖析之内部结构

前言本文是基于Java 8的HashMap进行分析,主要是介绍HashMap中的成员变量和类变量的用途,以及分析HashMap的数据结构。变量分析在HashMap中存在多个成员变量和类变量,搞清楚它们的用途有助于我们更深入了解HashMap,下面是它们的介绍: /** * 默认的初始容量,必须为2的次幂 / static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 总所周知是16 /* * 最大容量 / static final int MAXIMUM_CAPACITY = 1 << 30; /* * 默认的负载因子 / static final float DEFAULT_LOAD_FACTOR = 0.75f; /* * 将链表转化为红黑树的阈值,当链表节点数大于或等于该阈值-1则转化为红黑树 / static final int TREEIFY_THRESHOLD = 8; /* * 将红黑树转化为链表的阈值,当红黑树的节点小于该阈值时转化为链表 / static final int UNTREEIFY_THRESHOLD = 6; /* * 允许进行链表转化为红黑树的阈值,只有散列表大小大于或等于该值才能进行红黑树转化 / static final int MIN_TREEIFY_CAPACITY = 64; /* * HashMap中存储数据的数组,也称为散列表。 * 建议保持长度为2的次幂 / transient Node<K,V>[] table; /* * 缓存entrySet()方法的值 / transient Set<Map.Entry<K,V>> entrySet; /* * Map中键值对的个数 / transient int size; /* * HashMap数据结构被改变的次数,一般是指散列表的长度改变、Node链表增加或者减少节点 * 这个参数是用于快速失败机制 / transient int modCount; /* * 下一次触发调整大小(resize()方法)的阈值,一般为容量乘以负载因子 / int threshold; /* * 散列表的负载因子,用于计算扩容的阈值 */ final float loadFactor;数据结构HashMap使用拉链法解决哈希表中存在的哈希冲突问题,所以HashMap底层是用以Node组成的链表为元素的数组table来存储键值对,每个Node就是一个键值对对象。table称呼为散列表。而table对应的是散列表,是因为无论是存储还是读取键值对的时候,都会对key进行hash%table.length运算来进行散列表的命中,然后操作命中的索引对应的Node链表(还是会比较key和hash)。以上为Java 8之前版本的HashMap的实现,而Java 8进行了优化:就是当链表节点数超过阈值TREEIFY_THRESHOLD(8)时,则会将链表转化为红黑树。如果只是使用文字描述的话会很难理解,所以下面会通过一幅图展示: ...

February 11, 2019 · 1 min · jiezi

HashMap 常见应用:实现 SQL JOIN

在我的上一篇文章中,讲到了我自己初步认识 HashMap 的一个经验分享:HashMap 浅析 —— LeetCode Two Sum 刷题总结。作为一个 CRUD 工程师,平时很少接触到基础组件的涉及,那么是不是很难有机会用到 HashMap 呢?今天,就举一个常见的查询例子,来看看我们如何使用 HashMap 来提高代码的效率。已知一个 Student 类:public class Student { private Long id; private String name; public Student(Long id, String name) { this.id = id; this.name = name; } // —Getters And Setters—}和一个 Score 类:public class Score { private Long studentId; private String mathScore; private String englishScore; public Score(Long studentId, String mathScore, String englishScore) { this.studentId = studentId; this.mathScore = mathScore; this.englishScore = englishScore; } // —Getters And Setters—}我们需要把 Student 和 Score 合并到一起,即类 Report:public class Report { private Long studentId; private String studentName; private String mathScore; private String englishScore; public Report(Long studentId, String studentName, String mathScore, String englishScore) { this.studentId = studentId; this.studentName = studentName; this.mathScore = mathScore; this.englishScore = englishScore; } }看类的属性我们就明白了,这里其实相当于在 Student 和 Score 之间做一个 Join,得到 Report。这是我们在编程中常见的场景(例如查询了订单中心,用户中心,支付中心,合并各个中心返回的结果形成一个表单,因为各个中心是独立的微服务,无法使用 SQL JOIN)。现有两个 List:List<Student> 和 List<Score>:List<Student> students = Arrays.asList( new Student(1L, “Angle”), new Student(2L, “Baby”));List<Score> scores = Arrays.asList( new Score(1L, “90”, “87”), new Score(2L, “92”, “78”));在学会使用 HashMap 之前,我可能会做一次双重循环:List<Report> reports = new ArrayList<>();for (Student student : students) { for (Score score : scores) { if (!student.getId().equals(score.getStudentId())) { continue; } reports.add( new Report(student.getId(), student.getName(), score.getMathScore(), score.getEnglishScore()) ); break; }}时间复杂度最差的情况下是O(n * m)。但是使用 HashMap 来改善程序,就能得到不错的效果:Map<Long, Student> map = new HashMap<>();for (Student student : students) { map.put(student.getId(), student);}List<Report> reports = new ArrayList<>();for (Score score : scores) { Student student = map.get(score.getStudentId()); if(student == null){ // 避免 NPE continue; } reports.add( new Report(student.getId(), student.getName(), score.getMathScore(), score.getEnglishScore()) );}双重循环,变成了两次循环,时间复杂度是O(n + m)。显然要比前面的方法效果要好一些。笔者写了测试代码分别测试两个方法的效率,在 10w 数据下,执行时间如下:差距好像挺大。想了解为什么 HashMap 能够得到如此好的效果,可以看我的这篇文章:HashMap 浅析 —— LeetCode Two Sum 刷题总结。如果读者有更好的解法欢迎留言交流,笔者水平有限,在算法上研究不多。10w 数据的测试源码见下方,各位读者可以自行试验下效果:package com.xiangyu.demo.hashmap;import com.xiangyu.java.hashmap.Report;import com.xiangyu.java.hashmap.Score;import com.xiangyu.java.hashmap.Student;import org.junit.Before;import org.junit.Test;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;public class HashMapTest { private List<Student> students = new ArrayList<>(); private List<Score> scores = new ArrayList<>(); @Before public void before() { // 每个list 里放 10w 数据 for (long i = 0; i < 100000; i++) { students.add(new Student(i, “test”)); scores.add(new Score(i, “95”, “95”)); } } @Test public void TestHashMap() { Map<Long, Student> map = new HashMap<>(); for (Student student : students) { map.put(student.getId(), student); } List<Report> reports = new ArrayList<>(); for (Score score : scores) { Student student = map.get(score.getStudentId()); if (student == null) { continue; } reports.add( new Report(student.getId(), student.getName(), score.getMathScore(), score.getEnglishScore()) ); } System.out.println(reports.size()); } @Test public void testFor2() { List<Report> reports = new ArrayList<>(); for (Student student : students) { for (Score score : scores) { if (!student.getId().equals(score.getStudentId())) { continue; } reports.add( new Report(student.getId(), student.getName(), score.getMathScore(), score.getEnglishScore()) ); break; } } System.out.println(reports.size()); }} ...

January 26, 2019 · 2 min · jiezi

HashMap 浅析 —— LeetCode Two Sum 刷题总结

背景做了几年 CRUD 工程师,深感自己的计算机基础薄弱,在看了几篇大牛的分享文章之后,发现很多人都是通过刷 LeetCode 来提高自己的算法水平。的确,通过分析解决实际的问题,比自己潜心研究书本效率还是要高一些。一直以来遇到底层自己无法解决的问题,都是通过在 Google、GitHub 上搜索组件、博客来进行解决。这样虽然挺快,但是也让自己成为了一个“Ctrl+C/Ctrl+V”程序员。从来不花时间思考技术的内在原理。直到我刷了 Leetcode 第一道题目 Two Sum,接触到了 HashMap 的妙用,才激发起我去了解 HashMap 原理的兴趣。Two Sum(两数之和)TwoSum 是 Leetcode 中的第一道题,题干如下:给定一个整数数组nums和一个目标值target,请你在该数组中找出和为目标值的那两个整数,并返回他们的数组下标。你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。示例:给定 nums = [2, 7, 11, 15], target = 9因为 nums[0] + nums[1] = 2 + 7 = 9所以返回 [0, 1]初看这道题的时候,我当然是使用最简单的array遍历来解决了:public int[] twoSum(int[] nums, int target) { for (int i = 0; i < nums.length; i++) { for (int j = i + 1; j < nums.length; j++) { if (nums[j] == target - nums[i]) { return new int[] { i, j }; } } } throw new IllegalArgumentException(“No two sum solution”);}这个解法在官方称为“暴力法”。通过这个“暴力法”我们可以看到里面有个我们在编程中经常遇到的一个场景:检查数组中是否存在某元素。官方的解析中提到,哈希表可以保持数组中每个元素与其索引相互对应,所以如果我们使用哈希表来解决这个问题,可以有效地降低算法的时间复杂度。(不了解哈希表和时间复杂度的的朋友别急,下文会详细说明)使用哈希表的解法是这样的:public int[] twoSum(int[] nums, int target) { Map<Integer, Integer> map = new HashMap<>(); for (int i = 0; i < nums.length; i++) { int complement = target - nums[i]; if (map.containsKey(complement)) { return new int[] { map.get(complement), i }; } map.put(nums[i], i); } throw new IllegalArgumentException(“No two sum solution”);}即使我们不是很会算时间复杂度,也能够明显看到,原来的双重循环,在哈希表解法里,变成了单重循环,代码的效率很明显提升了。但是令人好奇的是map.containsKey()到底是用了什么样的魔力,实现快速判断元素complement是否存在呢?这里就要引出本篇文章的主角 —— HashMap。HashMap注:以下内容基于JDK 1.8进行讲解在了解map.containsKey()这个方法之前,我们还是得补习一下基础,毕竟笔者在看到这里得时候,对于哈希表、哈希值得概念也都忘得一干二净了。什么是哈希表呢?哈希表是根据键(Key)而直接访问在内存存储位置的数据结构维基上的解释比较抽象。我们可以把一张哈希表理解成一个数组。数组中可以存储Object,当我们要保存一个Object到数组中时,我们通过一定的算法,计算出来Object的哈希值(Hash Code),然后把哈希值作为下标,Object作为值保存到数组中。我们就得到了一张哈希表。看到这里,我们前文中说到的哈希表可以保持数组中每个元素与其索引相互对应,应该就很好理解了吧。回到 Leetcode 的代示例,map.containsKey()中显然是通过获取 Key 的哈希值,然后判断哈希值是否存在,间接判断 Key 是否已经存在的。到了这里,如果我们仅仅是想要能够明白 HashMap 的使用原理,基本上已经足够了。但是相信有不少朋友对它的哈希算法感兴趣。下面我详细解释一下。map.containsKey()解析我们查看 JDK 的源码,可以看到map.containsKey()中最关键的代码是这段:/** * 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) { 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); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }一上来看不懂没关系,其实这段代码最关键的部分就只有这一句:first = tab[(n - 1) & hash]) != null。其中tab是 HashMap 的 Node 数组(每个 Node 是一个 Key&value 键值对,用来存在 HashMap的数据),这里对数组的长度n和hash值,做&运算(至于为什么要进行这样的&运算,是与 HashMap 的哈希算法有关的,具体要看java.util.HashMap.hash()这个方法,哈希算法是数学家和计算机基础科学家研究的领域,这里不做深入研究),得到一个数组下标,这个下标对应的数组数据,一般情况下就是我们要找的节点。注意这里我说的是一般情况下,因为哈希算法需要兼顾性能与准确性,是有一定概率出现重复的情况的。我们可以看到上文getNode方法,有一段遍历的代码:do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e;} while ((e = e.next) != null);就是为了处理极端情况下哈希算法得到的哈希值没有命中,需要进行遍历的情况。在这个时候,时间复杂度是O(n),而在这种极端情况以外,时间复杂度是O(1),这也就是map.containsKey()效率比遍历高的奥秘。Tips:看到这里,如果有人问你:两个对象,其哈希值(hash code)相等,他们一定是同一个对象吗?相信你一定有答案了。(如果两个对象不同,但哈希值相等,这种情况叫哈希冲突)哈希算法通过前文我们可以发现,HashMap 之所以能够高效地根据元素找到其索引,是借助了哈希表的魔力,而哈希算法是 哈希表的灵魂。哈希算法实际上是数学家和计算机基础科学家研究的领域。对于我们普通程序员来说,并不需要研究太透彻。但是如果我们能够搞清楚其实现原理,相信对于今后的程序涉及大有裨益。按笔者的理解,哈希算法是为了给对象生成一个尽可能独特的Code,以方便内存寻址。此外其作为一个底层的算法,需要同时兼顾性能与准确性。为了更好地理解 hash 算法,我们拿java.lang.String的hash 算法来举例。java.lang.String hashCode方法:public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h;}相信这段代码大家应该都看得懂,使用 String 的 char 数组的数字每次乘以 31 再叠加最后返回,因此,每个不同的字符串,返回的 hashCode 肯定不一样。那么为什么使用 31 呢?在名著 《Effective Java》第 42 页就有对 hashCode 为什么采用 31 做了说明:之所以使用 31, 是因为他是一个奇素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算(低位补0)。使用素数的好处并不很明显,但是习惯上使用素数来计算散列结果。 31 有个很好的性能,即用移位和减法来代替乘法,可以得到更好的性能: 31 * i == (i << 5) - i, 现代的 VM 可以自动完成这种优化。这个公式可以很简单的推导出来。可以看到,使用 31 最主要的还是为了性能。当然用 63 也可以。但是 63 的溢出风险就更大了。那么15 呢?仔细想想也可以。在《Effective Java》也说道:编写这种散列函数是个研究课题,最好留给数学家和理论方面的计算机科学家来完成。我们此次最重要的是知道了为什么使用 31。java.util.HashMap hash 算法实现原理相对复杂一些,这篇文章:深入理解 hashcode 和 hash 算法,讲得非常好,建议大家感兴趣的话通篇阅读。 ...

January 22, 2019 · 3 min · jiezi

leetcode讲解--791. Custom Sort String

题目S and T are strings composed of lowercase letters. In S, no letter occurs more than once.S was sorted in some custom order previously. We want to permute the characters of T so that they match the order that S was sorted. More specifically, if x occurs before y in S, then x should occur before y in the returned string.Return any permutation of T (as a string) that satisfies this property.Example :Input: S = “cba"T = “abcd"Output: “cbad"Explanation: “a”, “b”, “c” appear in S, so the order of “a”, “b”, “c” should be “c”, “b”, and “a”. Since “d” does not appear in S, it can be at any position in T. “dcba”, “cdba”, “cbda” are also valid outputs.Note:S has length at most 26, and no character is repeated in S.T has length at most 200.S and T consist of lowercase letters only.题目地址讲解这道题刚开始一直思路挺混乱的,只知道一定得用HashMap来减少搜索字符的时间(遍历需要很多遍)。正确的思路是这样的:首先我们需要遍历一遍T,把其中的字符分为两类,一类是在S中有的,一类是在S中没有的。而且将它们出现的次数以HashMap这种数据结构记录下来。而这个分类的过程中,是需要搜索S的,所以我们要提前把S弄成一张哈希表,这样就不用遍历S很多遍了。最后我们遍历一遍S,按照顺序,将S中的元素又在mapT中的放到result中,同时放一个就remove一个缩小mapT的大小。然后再将剩下的mapT中的元素放到result中。Java代码class Solution { public String customSortString(String S, String T) { Map<Character, Integer> mapT = new HashMap<>(); Set<Character> mapS = new HashSet<>(); for(int i=0;i<S.length();i++){ mapS.add(S.charAt(i)); } for(int i=0;i<T.length();i++){ Integer count= mapT.get(T.charAt(i)); if(mapS.contains(T.charAt(i))){ if(count==null){ mapT.put(T.charAt(i),1); }else{ mapT.put(T.charAt(i),count+1); } }else{ if(count==null){ mapT.put(T.charAt(i),-1); }else{ mapT.put(T.charAt(i),count-1); } } } char[] result = new char[T.length()]; int indexBegin=0; int indexEnd = T.length()-1; for(int i=0;i<S.length();i++){ Integer count = mapT.get(S.charAt(i)); if(count!=null){ while(count>0){ result[indexBegin++] = S.charAt(i); count–; } mapT.remove(S.charAt(i)); } } for(Character c:mapT.keySet()){ Integer count = mapT.get(c); while(count<0){ result[indexEnd–] = c; count++; } } return String.valueOf(result); }} ...

December 31, 2018 · 2 min · jiezi

[LeetCode] 249. Group Shifted Strings

ProblemGiven a string, we can “shift” each of its letter to its successive letter, for example: “abc” -> “bcd”. We can keep “shifting” which forms the sequence:“abc” -> “bcd” -> … -> “xyz"Given a list of strings which contains only lowercase alphabets, group all strings that belong to the same shifting sequence.Example:Input: [“abc”, “bcd”, “acef”, “xyz”, “az”, “ba”, “a”, “z”],Output: [ [“abc”,“bcd”,“xyz”], [“az”,“ba”], [“acef”], [“a”,“z”]]Solutionclass Solution { public List<List<String>> groupStrings(String[] strings) { Map<String, List<String>> map = new HashMap<>(); for (String str: strings) { StringBuilder sb = new StringBuilder(); for (int i = 1; i < str.length(); i++) { int diff = str.charAt(i)-str.charAt(i-1); if (diff < 0) diff += 26; sb.append(‘a’+diff); //***** ‘a’ ***** } String key = sb.toString(); map.putIfAbsent(key, new ArrayList<>()); map.get(key).add(str); } return new ArrayList<>(map.values()); }}//[“an”,“bcf”]//keys: 110, 98100//result: [[“an”],[“bcf”]] ...

December 30, 2018 · 1 min · jiezi

leetcode讲解--811. Subdomain Visit Count

题目A website domain like “discuss.leetcode.com” consists of various subdomains. At the top level, we have “com”, at the next level, we have “leetcode.com”, and at the lowest level, “discuss.leetcode.com”. When we visit a domain like “discuss.leetcode.com”, we will also visit the parent domains “leetcode.com” and “com” implicitly.Now, call a “count-paired domain” to be a count (representing the number of visits this domain received), followed by a space, followed by the address. An example of a count-paired domain might be “9001 discuss.leetcode.com”.We are given a list cpdomains of count-paired domains. We would like a list of count-paired domains, (in the same format as the input, and in any order), that explicitly counts the number of visits to each subdomain.Example 1:Input: [“9001 discuss.leetcode.com”]Output: [“9001 discuss.leetcode.com”, “9001 leetcode.com”, “9001 com”]Explanation: We only have one website domain: “discuss.leetcode.com”. As discussed above, the subdomain “leetcode.com” and “com” will also be visited. So they will all be visited 9001 times.Example 2:Input: [“900 google.mail.com”, “50 yahoo.com”, “1 intel.mail.com”, “5 wiki.org”]Output: [“901 mail.com”,“50 yahoo.com”,“900 google.mail.com”,“5 wiki.org”,“5 org”,“1 intel.mail.com”,“951 com”]Explanation: We will visit “google.mail.com” 900 times, “yahoo.com” 50 times, “intel.mail.com” once and “wiki.org” 5 times. For the subdomains, we will visit “mail.com” 900 + 1 = 901 times, “com” 900 + 50 + 1 = 951 times, and “org” 5 times.Notes:The length of cpdomains will not exceed 100.The length of each domain name will not exceed 100.Each address will have either 1 or 2 “.” characters.The input count in any count-paired domain will not exceed 10000.The answer output can be returned in any order.题目地址讲解这道题我反了一个小错误,然后检查的时候都没检查出来。java代码class Solution { public List<String> subdomainVisits(String[] cpdomains) { Map<String, Integer> map = new HashMap<>(); List<String> result = new ArrayList<>(); for(String s:cpdomains){ int indexOfSpace = s.indexOf(’ ‘); Integer count = map.get(s.substring(indexOfSpace+1)); Integer add = Integer.parseInt(s.substring(0,indexOfSpace)); if(count==null){ map.put(s.substring(indexOfSpace+1), add); }else{ map.put(s.substring(indexOfSpace+1), count+add); } List<Integer> indexOfPoint = new ArrayList<>(); for(int i=s.length()-1;i>indexOfSpace;i–){ if(s.charAt(i)==’.’){ indexOfPoint.add(i); } } for(int i=0;i<indexOfPoint.size();i++){ String domainName = s.substring(indexOfPoint.get(i)+1); count = map.get(domainName); if(count==null){ map.put(domainName, add); }else{ map.put(domainName, count+add); } } } for(String key:map.keySet()){ result.add(map.get(key)+" “+key); } return result; }} ...

December 27, 2018 · 2 min · jiezi

[LeetCode] 245. Shortest Word Distance III

ProblemGiven a list of words and two words word1 and word2, return the shortest distance between these two words in the list.word1 and word2 may be the same and they represent two individual words in the list.Example:Assume that words = [“practice”, “makes”, “perfect”, “coding”, “makes”].Input: word1 = “makes”, word2 = “coding”Output: 1Input: word1 = “makes”, word2 = “makes"Output: 3Note:You may assume word1 and word2 are both in the list.Solutionclass Solution { public int shortestWordDistance(String[] words, String word1, String word2) { Map<String, List<Integer>> map = new HashMap<String, List<Integer>>(); for(int i = 0; i < words.length; i++) { String word = words[i]; if (!map.containsKey(word)) map.put(word, new ArrayList<>()); map.get(word).add(i); } if (word1.equals(word2)) { List<Integer> list = map.get(word1); if (list.size() < 2) return -1; int min = Integer.MAX_VALUE; for (int i = 0; i < list.size()-1; i++) { min = Math.min(min, list.get(i+1)-list.get(i)); } return min; } List<Integer> list1 = map.get(word1); List<Integer> list2 = map.get(word2); int min = Integer.MAX_VALUE; int i = 0, j = 0; while (i < list1.size() && j < list2.size()) { int index1 = list1.get(i), index2 = list2.get(j); if (index1 < index2) { min = Math.min(min, index2 - index1); i++; } else { min = Math.min(min, index1 - index2); j++; } } return min; }} ...

December 14, 2018 · 1 min · jiezi

[LeetCode] 244. Shortest Word Distance II

ProblemDesign a class which receives a list of words in the constructor, and implements a method that takes two words word1 and word2 and return the shortest distance between these two words in the list. Your method will be called repeatedly many times with different parameters. Example:Assume that words = [“practice”, “makes”, “perfect”, “coding”, “makes”].Input: word1 = “coding”, word2 = “practice”Output: 3Input: word1 = “makes”, word2 = “coding"Output: 1Note:You may assume that word1 does not equal to word2, and word1 and word2 are both in the list.Solutionclass WordDistance { private Map<String, List<Integer>> map; public WordDistance(String[] words) { map = new HashMap<String, List<Integer>>(); for(int i = 0; i < words.length; i++) { String word = words[i]; if (!map.containsKey(word)) map.put(word, new ArrayList<>()); map.get(word).add(i); } } public int shortest(String word1, String word2) { List<Integer> list1 = map.get(word1); List<Integer> list2 = map.get(word2); int min = Integer.MAX_VALUE; int i = 0, j = 0; while (i < list1.size() && j < list2.size()) { int index1 = list1.get(i), index2 = list2.get(j); if (index1 < index2) { min = Math.min(min, index2 - index1); i++; } else { min = Math.min(min, index1 - index2); j++; } } return min; }} ...

December 14, 2018 · 1 min · jiezi

猫头鹰的深夜翻译:Java WeakHashMap

本文简介WeakHashMap类概览WeakHashMap类构造器总结WeakHashMap类构造方法WeakHasjMap类使用举例1. WeakHashMap类概览WeakHashMap是一个实现了Map接口,并且键为weak型的哈希表。WeakHashMap中的条目不再被正常使用时,会被自动删除。它的键值均支持null。这个类类似于HashMap类,也具有初始容量和负载因子这样的效率参数。和绝大多数的集合类一样,这个类不是同步的。需要使用Collections.synchronizedMap方法来进行同步控制。弱引用–如果一个对象只有一个弱引用,那么垃圾回收期可以随时收回该对象的内存。它不需要等到系统内存不足时才回收。通常,它的内存会在下一次垃圾收集器运行时释放。2. WeakHashMap的构造器WeakHashMap(): 构造一个新的,空的WeakHashMap,初始容量为16,负载因子为0.75WeakHashMap(int initialCapacity): 构造一个新的,空的WeakHashMap, 初始容量为initialCapacity,负载因子为0.75WeakHashMap(int initialCapacity, float loadFactor): 构造一个新的,空的WeakHashMap, 初始容量为initialCapacity,负载因子为loadFactorWeakHashMap(Map<? extends K, ? extends V> m): 利用已有的map构造WeakHashMap3. WeakHashMap类的方法void clear(): 删除所有的条目boolean containsKey(Object key): 如果有该键,返回trueboolean containsValue(Object value): 如果有一个或多个value值,返回trueSet< Map.Entry<K,V>>entrySet(): 返回键值视图void forEach(BiConsumer<? super K,? super V> action): 对此映射中的每个条目执行给定操作,直到处理完所有条目或操作引发异常。V get(Object key): 返回指定键映射到的值,如果此映射不包含键的映射,则返回null。boolean isEmpty(): 如果此映射不包含键 - 值映射,则返回true。4. WeakHashMap类使用示例import java.util.Map;import java.util.Map.Entry;import java.util.WeakHashMap;public class WeakHashMapExample { public static void main(final String[] args) { final Map<Key, Project> map = new WeakHashMap<>(); Key key1 = new Key(“ACTIVE”); final Key key2 = new Key(“INACTIVE”); map.put(key1, new Project(100, “Customer Management System”, “Customer Management System”)); map.put(key2, new Project(200, “Employee Management System”, “Employee Management System”)); key1 = null; System.gc(); for (final Entry<Key, Project> entry : map.entrySet()) { System.out.println(entry.getKey().getKey() + " " + entry.getValue()); } }}class Key { private String key; public Key(final String key) { super(); this.key = key; } public String getKey() { return key; } public void setKey(final String key) { this.key = key; }}输出:INACTIVE [project id : 200, project name : Employee Management System, project desc : Employee Management System ] ...

December 7, 2018 · 1 min · jiezi

一次 HashSet 所引起的并发问题

背景上午刚到公司,准备开始一天的摸鱼之旅时突然收到了一封监控中心的邮件。心中暗道不好,因为监控系统从来不会告诉我应用完美无 bug,其实系统挺猥琐。打开邮件一看,果然告知我有一个应用的线程池队列达到阈值触发了报警。由于这个应用出问题非常影响用户体验;于是立马让运维保留现场 dump 线程和内存同时重启应用,还好重启之后恢复正常。于是开始着手排查问题。分析首先了解下这个应用大概是做什么的。简单来说就是从 MQ 中取出数据然后丢到后面的业务线程池中做具体的业务处理。而报警的队列正好就是这个线程池的队列。跟踪代码发现构建线程池的方式如下:ThreadPoolExecutor executor = new ThreadPoolExecutor(coreSize, maxSize, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());; put(poolName,executor);采用的是默认的 LinkedBlockingQueue 并没有指定大小(这也是个坑),于是这个队列的默认大小为 Integer.MAX_VALUE。由于应用已经重启,只能从仅存的线程快照和内存快照进行分析。内存分析先利用 MAT 分析了内存,的到了如下报告。其中有两个比较大的对象,一个就是之前线程池存放任务的 LinkedBlockingQueue,还有一个则是 HashSet。当然其中队列占用了大量的内存,所以优先查看,HashSet 一会儿再看。由于队列的大小给的够大,所以结合目前的情况来看应当是线程池里的任务处理较慢,导致队列的任务越堆越多,至少这是目前可以得出的结论。线程分析再来看看线程的分析,这里利用 fastthread.io 这个网站进行线程分析。因为从表现来看线程池里的任务迟迟没有执行完毕,所以主要看看它们在干嘛。正好他们都处于 RUNNABLE 状态,同时堆栈如下:发现正好就是在处理上文提到的 HashSet,看这个堆栈是在查询 key 是否存在。通过查看 312 行的业务代码确实也是如此。这里的线程名字也是个坑,让我找了好久。定位分析了内存和线程的堆栈之后其实已经大概猜出一些问题了。这里其实有一个前提忘记讲到:这个告警是凌晨三点发出的邮件,但并没有电话提醒之类的,所以大家都不知道。到了早上上班时才发现并立即 dump 了上面的证据。所有有一个很重要的事实:这几个业务线程在查询 HashSet 的时候运行了 6 7 个小时都没有返回。通过之前的监控曲线图也可以看出:操作系统在之前一直处于高负载中,直到我们早上看到报警重启之后才降低。同时发现这个应用生产上运行的是 JDK1.7 ,所以我初步认为应该是在查询 key 的时候进入了 HashMap 的环形链表导致 CPU 高负载同时也进入了死循环。为了验证这个问题再次 review 了代码。整理之后的伪代码如下://线程池private ExecutorService executor;private Set<String> set = new hashSet();private void execute(){ while(true){ //从 MQ 中获取数据 String key = subMQ(); executor.excute(new Worker(key)) ; }}public class Worker extends Thread{ private String key ; public Worker(String key){ this.key = key; } @Override private void run(){ if(!set.contains(key)){ //数据库查询 if(queryDB(key)){ set.add(key); return; } } //达到某种条件时清空 set if(flag){ set = null ; } } }大致的流程如下:源源不断的从 MQ 中获取数据。将数据丢到业务线程池中。判断数据是否已经写入了 Set。没有则查询数据库。之后写入到 Set 中。这里有一个很明显的问题,那就是作为共享资源的 Set 并没有做任何的同步处理。这里会有多个线程并发的操作,由于 HashSet 其实本质上就是 HashMap,所以它肯定是线程不安全的,所以会出现两个问题:Set 中的数据在并发写入时被覆盖导致数据不准确。会在扩容的时候形成环形链表。第一个问题相对于第二个还能接受。通过上文的内存分析我们已经知道这个 set 中的数据已经不少了。同时由于初始化时并没有指定大小,仅仅只是默认值,所以在大量的并发写入时候会导致频繁的扩容,而在 1.7 的条件下又可能会形成环形链表。不巧的是代码中也有查询操作(contains()),观察上文的堆栈情况:发现是运行在 HashMap 的 465 行,来看看 1.7 中那里具体在做什么:已经很明显了。这里在遍历链表,同时由于形成了环形链表导致这个 e.next 永远不为空,所以这个循环也不会退出了。到这里其实已经找到问题了,但还有一个疑问是为什么线程池里的任务队列会越堆越多。我第一直觉是任务执行太慢导致的。仔细查看了代码发现只有一个地方可能会慢:也就是有一个数据库的查询。把这个 SQL 拿到生产环境执行发现确实不快,查看索引发现都有命中。但我一看表中的数据发现已经快有 7000W 的数据了。同时经过运维得知 MySQL 那台服务器的 IO 压力也比较大。所以这个原因也比较明显了:由于每消费一条数据都要去查询一次数据库,MySQL 本身压力就比较大,加上数据量也很高所以导致这个 IO 响应较慢,导致整个任务处理的就比较慢了。但还有一个原因也不能忽视;由于所有的业务线程在某个时间点都进入了死循环,根本没有执行完任务的机会,而后面的数据还在源源不断的进入,所以这个队列只会越堆越多!这其实是一个老应用了,可能会有人问为什么之前没出现问题。这是因为之前数据量都比较少,即使是并发写入也没有出现并发扩容形成环形链表的情况。这段时间业务量的暴增正好把这个隐藏的雷给揪出来了。所以还是得信墨菲他老人家的话。总结至此整个排查结束,而我们后续的调整措施大概如下:HashSet 不是线程安全的,换为 ConcurrentHashMap同时把 value 写死一样可以达到 set 的效果。根据我们后面的监控,初始化 ConcurrentHashMap 的大小尽量大一些,避免频繁的扩容。MySQL 中很多数据都已经不用了,进行冷热处理。尽量降低单表数据量。同时后期考虑分表。查数据那里调整为查缓存,提高查询效率。线程池的名称一定得取的有意义,不然是自己给自己增加难度。根据监控将线程池的队列大小调整为一个具体值,并且要有拒绝策略。升级到 JDK1.8。再一个是报警邮件酌情考虑为电话通知????。HashMap 的死循环问题在网上层出不穷,没想到还真被我遇到了。现在要满足这个条件还是挺少见的,比如 1.8 以下的 JDK 这一条可能大多数人就碰不到,正好又证实了一次墨菲定律。同时我会将文章更到这里,方便大家阅读和查询。https://crossoverjie.top/JCSprout/你的点赞与分享是对我最大的支持 ...

November 8, 2018 · 1 min · jiezi