安庆大理运城常德铜陵江西
投稿投诉
江西南阳
嘉兴昆明
铜陵滨州
广东西昌
常德梅州
兰州阳江
运城金华
广西萍乡
大理重庆
诸暨泉州
安庆南充
武汉辽宁

轻松理解HashMap

1月21日 小米粒投稿
  HashMap概述
  基于哈希表的Map接口的实现。此实现提供所有可选的映射操作,并允许使用null值和null键。(除了非同步和允许使用null之外,HashMap类与Hashtable大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get和put)提供稳定的性能。迭代collection视图所需的时间与HashMap实例的容量(桶的数量)及其大小(键值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。百度百科
  HashMap是Java程序员使用频率最高的用于映射(键值对)处理的数据类型(键值对集合)。随着JDK版本的更新,JDK1。8对HashMap底层的实现进行了优化,例如引入红黑树的数据结构和扩容的优化等。文章先基于1。7描述,最后再提1。8与之更改的地方。HashMapString,StringhashMapnewHashMap();hashMap。put(张三,男);hashMap。get(张三);
  那么它里面存的元素就key和value么?(它其实里面封装成一个entry可以看到除了key与value之外还有next属性,所以HaspMap里的元素不仅仅含有键值对还有指向下一节点的元素的信息)staticclassEntryK,VimplementsMap。EntryK,V{finalKKeyvalue结构的keyV存储值EntryK,V指向下一个链表节点哈希值}HashMap的数据结构
  在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap底层就是一个数组结构,数组中的每一项又是一个链表。在1。8之后当链表长度大于等于8时转为红黑树(自平衡的二叉树这里不详细展开,以后有机会再讲)。
  那么它是如何去存储?(每个元素存进来如何找到内置数组的位置)hashMap。put(战三,work);hashMap。put(里的,energer);hashMap。put(约翰,cookie);。。。。
  假如和ArrayList一样初始化一个index变量值为0作为下标,当arrayList。add(a)时将元素存在内置数组array〔index〕再index自增。即每次往后添加元素。至到数组满了再依次从第一个往后添加链。
  在这种情况下我们去hashMap。get(约翰),怎么去找到。很显然没办法定位。只能从数组第一位开始找比较entry的key值如不同再下一个比较直到找到相同的key。那么数量很多而且对应的entry位置比较偏后查找是及其费时的。虽然添加没毛病速度也快但查找是遍历比较所以是不合理的。
  在HashMap当中实际上存储时它会去给key进行类似hash得到的hashCode与数组的容量取模的得到的就是数组的位置在这样的位置存下。
  在使用key查找时也是对查找的key进行相同的hash直接定位到当初存的位置。这样的一个位置在数组是随机的,不同的code取模有可能会出现相同的位置即形成链表。然后在链表当中找到对应key的entry返回。下面伪代码描述了它的get方法的一个大概意思。直接找到数组所在的entry如果不是就是该entry的next。如果还不是就是next的next直到找到对应key的entry。get(key){inthashcodekey。hashCode();intindexhashcodetable。Entryentrytable〔index〕;。。。}
  那么在这里所谓的链表就是在逻辑上理解为它们在数组同一个位置,实际上就是先找到数组的这个位置得到了一个entry1然后这个entry1里的next属性引用的就是一个entry2,通过这个entry2的next就能找到entry3。这样的一个链表,基于找到第一个(数组中的entry)然后通过next字段一个一个的引用。下图与上图相对应
  上图数组存的entry3变量引用堆中的0x003的一个entry对象,这个对象里的next属性引用了地址0x004的entry对象。put时它是去怎样去插入的。当发生碰撞是找到这个链表的最下的entry把它的nextnull换成当前插入的entry地址,还是把当前插入的entry的next改为第一个就是数组中的table〔index〕。也就是头插还是尾插。
  那么怎样去存下面使用伪代码说明大概流程(实际源码还有判断空判断key是否重复先忽略)
  假设是尾插put(key,value){intcodekey。hashCode();intindexcodetable。Entryetable〔index〕;1。数组当前位置空table〔index〕newEntry(key,value,null);2。当前位置有entry就将它的next设为当前创建的entrytable〔index〕。nextnewEntry(key,value,null);3。当前有entry并且next也有table〔index〕。next。nextnewEntry(key,value,null);遍历链表找到最后一个entry也就是next为空的entry,将它的next设成当前创建的entrywhile(e!null){ee。}e。nextnewEntry(key,value,null);}
  假设是头插put(key,value){intcodekey。hashCode();intindexcodetable。1。new当前的entry,并将next设成现在第一个也就是数组上的table〔index〕2。将当前的entry设成第一个就是放在数组上。它的next就是以前的第一个entry对象,现在把当前对象放数组table〔index〕newentry(key,value,table〔index〕);}
  两种方式首先头插法明显要快。直接插然后下移只要一句代码而尾插需要遍历。所以我们在jdk1。7当中是的确使用的头插法,但在1。8之后修改成尾插法下面会提。容量问题publicclassHashMapK,VextendsAbstractMapK,VimplementsMapK,V,Cloneable,Serializable{初始默认容量16staticfinalintDEFAULTINITIALCAPACITY14;最大容量值staticfinalintMAXIMUMCAPACITY130;默认加载因子staticfinalfloatDEFAULTLOADFACTOR0。75f;。。。。。。}
  默认容量16,加载因子0。75。通过构造方法可以自己传入容量与加载因子。但值得一提的是容量一定是2的指数幂Inflatesthetable。privatevoidinflateTable(inttoSize){Findapowerof2toSizeintcapacityroundUpToPowerOf2(toSize);threshold(int)Math。min(capacityloadFactor,MAXIMUMCAPACITY1);tablenewEntry〔capacity〕;initHashSeedAsNeeded(capacity);}
  为什么它的容量一定要求为2的指数幂呢。桶数组用来去定位存储,hashCode与容量取模定位。确实不管是不是2的指数幂都能进行取模运算,因此这个要求大概是与效率有关。可以看到源码得index的方式并不是indexhlengthindexFor(h,length){indexh(length1);}
  那么就可能是在容量位2的指数幂的情况下通过这样的运算就能代替取模运算,并且位运算与减运算效率高于除运算。下面可以试一下:假如入容量16,一个随机hashCodehashCode101100100111length000000001111index000000000111假如入容量10,一个随机hashCodehashCode101100100111length000000001001index000000000001
  通过与运算结果肯定是小于的。所以结果不会越界,但如果容量不为2的指数幂有某些下标是永远取不到的,在例子二中比容量1001(10)小的是09也就是全部的下标。但111(7),101(5),11(3)永远取不到。所以只有满足2的指数幂减一与运算的结果才是具有全部下标的可能性和取模实际一样。扩容之后所有元素也要遍历重新定位这个时候也要进行大量的这个运算所以采用这个h(length1)效率要更高。加载因子
  第二就是加载因子为何是0。75,这个大家可能都知道等桶数组满了再扩容哈希碰撞一定挺多的很容易长链1。8会生成红黑树,但装到一半就扩容就空位太多很浪费空间,0。75也就是这样的一个折中的选择。总而言之就是这样规定。也是通过统计出来的比较好的结果。下面这段注释也说明了取0。75发生8次碰撞的概率已经是一亿分之六红黑树并不会那么容易生成那样就够了,其实关于为什么是0。75就把他当做一个折中的选择。这里包括下面注释内容其实上并没有说明,这个东西泊松分布和红黑树为什么在设定在链表到8的时候生成有关系下面会讲BecauseTreeNodesareabouttwicethesizeofregularnodes,weusethemonlywhenbinscontainenoughnodestowarrantuse(seeTREEIFYTHRESHOLD)。Andwhentheybecometoosmall(duetoremovalorresizing)theyareconvertedbacktoplainbins。InusageswithwelldistributeduserhashCodes,treebinsarerarelyused。Ideally,underrandomhashCodes,thefrequencyofnodesinbinsfollowsaPoissondistribution(http:en。wikipedia。orgwikiPoissondistribution)withaparameterofabout0。5onaverageforthedefaultresizingthresholdof0。75,althoughwithalargevariancebecauseofresizinggranularity。Ignoringvariance,theexpectedoccurrencesoflistsizekare(exp(0。5)pow(0。5,k)factorial(k))。Thefirstvaluesare:0:0。606530661:0。303265332:0。075816333:0。012636064:0。001579525:0。000157956:0。000013167:0。000000948:0。00000006扩容问题
  扩容源码(1。7)voidresize(intnewCapacity){Entry〔〕oldTintoldCapacityoldTable。。。。。。。创建一个新的HashTableEntry〔〕newTablenewEntry〔newCapacity〕;将OldHashTable上的数据迁移到NewHashTable上transfer(newTable);tablenewTthreshold(int)(newCapacityloadFactor);}put时当达到容量的0。75,进行扩容resize方法创建新数组后就是调用transfervoidtransfer(Entry〔〕newTable,booleanrehash){intnewCapacitynewTable。新数组的长度for(EntryK,Ve:table){遍历旧数组while(null!e){遍历旧数组中的每个链表结点EntryK,Vnexte。next指向当前遍历结点e的下一个结点if(rehash){再hashe。hashnulle。key?0:hash(e。key);}intiindexFor(e。hash,newCapacity);重新计算当前元素在新数组中的位置关键的3行代码(头插法移动元素)e。nextnewTable〔i〕;newTable〔i〕e;}}}
  put时当达到阈值后进行扩容也就是执行resize方法创建新数组,再执行transfer方法里面双循环遍历数组与链表,重新定位放入新桶数组,在1。7的扩容会有死环问题,这里先根据源码的扩容步骤作出了如下的扩容步骤图
  扩容流程
  假设上图数组正在扩容,首先循环到null!e的时候才能执行while的内容,e是遍历table数组的元素所以efirst,nexte。next这时代码中的nextsec。
  然后求e。hash重新计算使用indexFor得到新数组的下标i(假设为3,第4个位置),让e。next改为newTable〔i〕的引用,再将newTable〔i〕改为当前e也就是fist,也就是头插法
  代码第三步enext将变量e的引用原先是fist改为next的引用也就是esec。因此while里面就是遍历链表的元素赋值给e,在判断e是否为空当前e是sec,再进行同样的操作(定位,头插,移位)
  现在新数组就变成图中右下的样子了,可以看到就是sec的next变成first,first的next为空比起原先数组的关系就是反过来的。因为头插嘛第一个先插过去后面的插过去就更靠前第一个就是最下面。接下来e就是sec的next也就是遍历到第三个了然后没有下一个while结束,for循环下一个数组元素。就这样循环往复遍历所有的元素转移到新数组。
  死环问题就是在并发的情况下做这些扩容步骤出现的问题,通过上述演示单线程的一个扩容流程。假设现在两个线程同时对一个hashmap扩容。那么同时访问resize方法两个线程里面都创建了一个自己线程里的新数组,然后再执行transfer方法共同执行到while里的第一句nexte。next时的情况如下图。两个线程里都有自己的e与next还有新数组。
  这时线程1停在这里。线程2继续全部执行完(while循环两次直到e为空),这时线程2的新数组已经移完原来的两个元素
  第一次循环
  第二次循环
  这时候线程1又往后开始执行了。其实问题就在于此时堆中的fisrt对象和sec已经被改了。first对象里的next属性现在其实是空,然后sec里的next值为first。按照步骤
  第一步完成后e(first)到新数组,之后e改为next(sec)。
  到这一步问题就来了,处理完first之后现在e引用sec,再次while循环得nexte。next。现在e。nextfirst而不是null,就出现循环了。处理完sec到时候e再引用first再循环一遍之后e才为空
  将sec移过去然后next为first(其实它的next本来就是first了因为开始那个线程已经改了因此前面next就不等于null,而是first)。导致接下来e不是null,是first。然后再进while循环efirst,nextnull。这时first就要插在sec上就是first的next改为sec。就形成了死循环
  总结这个问题产生就是因为头插法导致从旧数组到新数组的时候链表方向会反过来,再因为并发的问题开始读取的是first并且next是sec。但中途却被别的线程改了因为扩容next反过来。它还拿着之前的循环一次再取就是有问题了本来应该链表结束了为0,结果可以连到fisrt。JDK8的优化resize扩容优化解决了resize时多线程死循环问题引入了红黑树,目的是避免单条链表过长而影响查询效率
  关于resize优化,在1。7上面扩容源码里拿到元素重新类似取模运算h(length1)得到位置,实际上在扩容是原来的两倍,这个时候取模结果就是原来的位置或原来的位置加原数组长度就这两种情况。扩容前长度16length1000000001111hashCode1101100100111(111)3hashCode2000101010111(111)3扩容后长度32length1000000011111hashCode1101100100111(111)3hashCode2110101010111(10111)19
  所以hashcode之前没用到的高一位要么是0要么是1。新数组容量是以前两倍高位多了个1,hashcode如果高一位是0和以前就没有区别,如果是1就多了1一个原数组大小。从以前的111要么没有要么还是111要么是10111就是看那个高位所以我们只需要判断hlength的结果。就看那一个位就可以判断它的新下标是不变还是加上老数组容量扩容前长度16length000000010000hashCode1101100100111(00000000)0hashCode2000101010111(00010000)16这段代码放在遍历原数组里面if((hlength)0){当前遍历的下标i就是新数组下标}else{当前遍历的ioldTable。length就是新下标}
  1。8源码当中也就是这样去确定新的下标位置NodeK,VloHeadnull,loTNodeK,VhiHeadnull,hiTNodeK,Vdo{nexte。判断是低指针还是高指针if((e。hasholdCap)0){if(loTailnull)loHelseloTail。loT}else{if(hiTailnull)hiHelsehiTail。hiT}}while((enext)!null);if(loTail!null){loTail。newTab〔j〕loH}if(hiTail!null){hiTail。newTab〔joldCap〕hiH}
  在上述源码中,除了新坐标的计算外。内部定义的两组共四个指针(低头、低尾、高头、高尾)低高区分新下标是保持原样的还是增加的和上面提的一样不需要rehash,无论是低指针还是高指针都有两个头尾指针head、tail,标记头尾也是解决1。7当中由于体位倒置会出现的死循环问题
投诉 评论 转载

大会2019全球智博会将于苏州召开,三大重磅奖项候选项目火热AI科技评论按,2019全球人工智能产品应用博览会(以下简称2019全球智博会)将于今年5月9日至11日在苏州工业园区举办,今年是智博会举办第二年,主办方将秉承上届大会的成功经……麒麟芯片从高到底排名,麒麟980和麒麟810,谁的意义更重要考虑到麒麟芯片比较多,比较老的产品因为性能落后已经没有讨论的必要了,本文只对近几年的中高端麒麟芯片进行对比,根据安兔兔跑分成绩,简单地分成了4个梯队,其中第一梯队的安兔兔跑分6……如果联想没有中国会怎样?这个题,是伪命题,如果没有联想,中国可能芯片的难题早就解决了。联想拖了中国的芯片的后腿。联想从各方面的报道,就是把国有资产变成私有财产。关于创新发展方面对中国的发展跟本没有关系……新车三电机版本续航近1000公里,特斯拉Cybertruck文:懂车帝原创曹浩〔懂车帝原创产品〕日前,特斯拉向美国专利商标局(USPTO)提交了一项Cybertruck中控屏界面UI专利图。从专利图中我们可以看到多项与Cybert……中国大陆禁止全部境外包裹?联邦快递UPS回应持续运营,部分地每经记者:李卓每经实习记者:杨昕怡每经编辑:刘雪梅近日,有网传消息称,多个国家快递公司同时宣布暂停发往中国大陆地区的快递服务。该消息表示,UPS、CanadaPost、日……华为再推老旗舰内存升级服务,鸿蒙OS大内存,为老机型完美续命6月2号晚间,华为带来了正式版的鸿蒙OS2。0,同时也带来了多款新品。对于更多的华为老用户来说,华为新旗舰价格太贵,常规机型也很难更新,所以老机型是否可以顺利升级鸿蒙OS2。0……华为新款折叠屏手机MateXs2,4月28日发布华为官宣,将于4月28日发布新款折叠屏手机MateXs2。此前华为曾先后发布MateX、MateXs、MateX2、MateX2典藏版等折叠屏手机。网友评论:TYc……现有世界的孪生空间当下每个人脑袋里都会时不时的被灌入云空间、云存储、区块链、智慧城市、智能AI、物联网、车联网、5G等等当下火热的概念。有人认为概念炒作,有人担心机器替换人力,有人激情澎湃跟着手……轻松理解HashMapHashMap概述基于哈希表的Map接口的实现。此实现提供所有可选的映射操作,并允许使用null值和null键。(除了非同步和允许使用null之外,HashMap类与Ha……河南省直单位租车新规鼓励租用新能源车,轿车限18万元以内【大河报大河财立方】(记者杨志莹通讯员梁金鼎)公车改革后,省直单位公务出行的多样化用车需求如何满足?近日,河南省机关事务管理局运用信息技术建立省直公务出行租赁平台,并与省财政厅……巧妙的使用tcpdump查看原始数据包使用tcpdump查看原始数据包虽然像Snort这样的工具在筛选通过我们网络的所有内容方面做得非常出色,但有时需要查看原始数据。为此,我们最好的工具是tcpdump。……赵明突然官宣,一切变化太突然,恐怕是市面上最好的荣耀折叠手机折叠手机的出现,一方面是各大手机厂商展示自家研发能力的表现,另一个方面是为了解决手机设计同质化的难题。从三星ZFlip和华为MateX开始,市面上后续又出现了小米MIXFold……
音响和低音炮有区别吗?音箱有低音喇叭,为什么还要低音炮?中国科学报中文域名的故事步入社会之后,你还记得自己人生中第一部手机是什么吗?笔记本与平板二合一一台机器享受两种功能纵容我的另一面新蔚揽旅行轿车正式上市黄光裕的出狱新零售市场的一剂兴奋剂你喜欢手机还是单反的拍照效果,手机摄影与相机摄影的差别在哪?投诉中国移动四问四答关于托管数据中心的那些事儿软件专盯跳槽者,背后仍是个人隐私保护之困丨长江评论河南受灾,华为又捐款又出力,只是没宣传django3批量修改的用法如何在邮件正文添加简历对你绝情的男人还能回头吗这4种表现说明他想回头受著作权法保护的作品从作品完成之日自动获得吗?风趣俏皮个性签名一触即发造句用一触即发造句大全我们的小指也拴着红线受疫情影响首当其冲的餐饮业主该怎么做人类的反省有亚健康怎么办情感测试:看你们俩是否般配不被对方察觉手机定位苹果(苹果手机如何不被定位跟踪)有哪些高校心理社团入驻了心理圈?

友情链接:中准网聚热点快百科快传网快生活快软网快好知文好找七猫云易事利