范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

轻松理解HashMap

  HashMap概述
  基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的"容量"(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。——百度百科
  HashMap是Java程序员使用频率最高的用于映射(键值对)处理的数据类型(键值对集合)。随着JDK版本的更新,JDK1.8对HashMap底层的实现进行了优化,例如引入红黑树的数据结构和扩容的优化等。文章先基于1.7描述,最后再提1.8与之更改的地方。  HashMap hashMap = new HashMap(); hashMap.put("张三","男"); hashMap.get("张三");
  那么它里面存的元素就key和value么?(它其实里面封装成一个entry可以看到除了key与value之外还有next属性,所以HaspMap里的元素不仅仅含有键值对还有指向下一节点的元素的信息)  static class Entry implements Map.Entry {       final K key;//Key-value结构的key       V value;//存储值       Entry next;//指向下一个链表节点       final int hash;//哈希值 }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){     int hashcode = key.hashCode();     int index = hashcode%table.length;     Entry entry = table[index];     ...     return entry; }
  那么在这里所谓的链表就是在逻辑上理解为它们在数组同一个位置,实际上就是先找到数组的这个位置得到了一个entry1然后这个entry1里的next属性引用的就是一个entry2,通过这个entry2的next就能找到entry3。这样的一个链表,基于找到第一个(数组中的entry)然后通过next字段一个一个的引用。下图与上图相对应
  上图数组存的entry3变量引用堆中的0x003的一个entry对象,这个对象里的next属性引用了地址0x004的entry对象。put时它是去怎样去插入的。当发生碰撞是找到这个链表的最下的entry把它的next=null换成当前插入的entry地址,还是把当前插入的entry的next改为第一个就是数组中的table[index]。也就是头插还是尾插。
  那么怎样去存下面使用伪代码说明大概流程(实际源码还有判断空判断key是否重复先忽略)
  假设是尾插  put(key,value){     int code = key.hashCode();     int index = code%table.length;     Entry e = table[index];     /*     1.数组当前位置空     table[index] = new Entry(key,value,null);     2.当前位置有entry就将它的next设为当前创建的entry     table[index].next = new Entry(key,value,null);     3.当前有entry并且next也有     table[index].next.next = new Entry(key,value,null);     遍历链表找到最后一个entry也就是next为空的entry,将它的next设成当前创建的entry     */     while(e!=null){ 	e = e.next;     }     e.next = new Entry(key,value,null); }
  假设是头插  put(key,value){     int code = key.hashCode();     int index = code%table.length;     //1.new当前的entry,并将next设成现在第一个也就是数组上的table[index]     //2.将当前的entry设成第一个就是放在数组上。     //它的next就是以前的第一个entry对象,现在把当前对象放数组     table[index] = new entry(key,value,table[index]); }
  两种方式首先头插法明显要快。直接插然后下移只要一句代码而尾插需要遍历。所以我们在jdk1.7当中是的确使用的头插法,但在1.8之后修改成尾插法下面会提。  容量问题public class HashMap     extends AbstractMap     implements Map, Cloneable, Serializable {     // 初始默认容量 16     static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;      // 最大容量值     static final int MAXIMUM_CAPACITY = 1 << 30;      // 默认加载因子     static final float DEFAULT_LOAD_FACTOR = 0.75f;      ...... }
  默认容量16,加载因子0.75。通过构造方法可以自己传入容量与加载因子。但值得一提的是容量一定是2的指数幂  /** * Inflates the table. */ private void inflateTable(int toSize) {     // Find a power of 2 >= toSize     int capacity = roundUpToPowerOf2(toSize);     threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY+1);     table = new Entry[capacity];     initHashSeedAsNeeded(capacity); }
  为什么它的容量一定要求为2的指数幂呢。桶数组用来去定位存储,hashCode与容量取模定位。确实不管是不是2的指数幂都能进行取模运算,因此这个要求大概是与效率有关。可以看到源码得index的方式  //并不是index = h % length indexFor(h,length){     index = h & (length - 1); }
  那么就可能是在容量位2的指数幂的情况下通过这样的运算就能代替取模运算,并且位运算与减运算效率高于除运算。下面可以试一下:  //假如入容量16,一个随机hashCode hashCode 1011 0010 0111 length   0000 0000 1111 index    0000 0000 0111//假如入容量10,一个随机hashCode hashCode 1011 0010 0111 length   0000 0000 1001 index    0000 0000 0001
  通过与运算结果肯定是小于的。所以结果不会越界,但如果容量不为2的指数幂有某些下标是永远取不到的,在例子二中比容量1001(10)小的是0-9也就是全部的下标。但111(7),101(5),11(3)永远取不到。所以只有满足2的指数幂减一与运算的结果才是具有全部下标的可能性和取模实际一样。扩容之后所有元素也要遍历重新定位这个时候也要进行大量的这个运算所以采用这个h&(length-1)效率要更高。  加载因子
  第二就是加载因子为何是0.75,这个大家可能都知道等桶数组满了再扩容哈希碰撞一定挺多的很容易长链1.8会生成红黑树,但装到一半就扩容就空位太多很浪费空间,0.75也就是这样的一个折中的选择。总而言之就是这样规定。也是通过统计出来的比较好的结果。下面这段注释也说明了取0.75发生8次碰撞的概率已经是一亿分之六红黑树并不会那么容易生成那样就够了,其实关于为什么是0.75就把他当做一个折中的选择。这里包括下面注释内容其实上并没有说明,这个东西泊松分布和红黑树为什么在设定在链表到8的时候生成有关系下面会讲  * Because TreeNodes are about twice the size of regular nodes, we * use them only when bins contain enough nodes to warrant use * (see TREEIFY_THRESHOLD). And when they become too small (due to * removal or resizing) they are converted back to plain bins.  In * usages with well-distributed user hashCodes, tree bins are * rarely used.  Ideally, under random hashCodes, the frequency of * nodes in bins follows a Poisson distribution * (http://en.wikipedia.org/wiki/Poisson_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 扩容问题
  扩容源码(1.7)  void resize(int newCapacity) {     Entry[] oldTable = table;     int oldCapacity = oldTable.length;     ......     //创建一个新的Hash Table     Entry[] newTable = new Entry[newCapacity];     //将Old Hash Table上的数据迁移到New Hash Table上     transfer(newTable);     table = newTable;     threshold = (int)(newCapacity * loadFactor); }//put时当达到容量的0.75,进行扩容resize方法创建新数组后就是调用transfer void transfer(Entry[] newTable, boolean rehash) {         int newCapacity = newTable.length;//新数组的长度         for (Entry e : table) {//遍历旧数组             while(null != e) {//遍历旧数组中的每个链表结点                 Entry next = e.next;//next指向当前遍历结点e的下一个结点                 if (rehash) {//再hash                     e.hash = null == e.key ? 0 : hash(e.key);                 }                 int i = indexFor(e.hash, newCapacity);//重新计算当前元素在新数组中的位置                 /*********关键的 3行代码(头插法移动元素)*********/                 e.next = newTable[i];                 newTable[i] = e;                 e = next;             }         }     }
  put时当达到阈值后进行扩容也就是执行resize方法创建新数组,再执行transfer方法里面双循环遍历数组与链表,重新定位放入新桶数组,在1.7的扩容会有死环问题,这里先根据源码的扩容步骤作出了如下的扩容步骤图
  扩容流程
  假设上图数组正在扩容,首先循环到null != e的时候才能执行while的内容,e是遍历table数组的元素所以e = first,next = e.next这时代码中的next = sec。
  然后求e.hash重新计算使用indexFor得到新数组的下标i(假设为3,第4个位置),让e.next改为newTable[i]的引用,再将newTable[i]改为当前e也就是fist,也就是头插法
  代码第三步e = next 将变量e的引用原先是fist改为next的引用也就是e = sec。因此while里面就是遍历链表的元素赋值给e,在判断e是否为空当前e是sec,再进行同样的操作(定位,头插,移位)
  现在新数组就变成图中右下的样子了,可以看到就是sec的next变成first,first的next为空比起原先数组的关系就是反过来的。因为头插嘛第一个先插过去后面的插过去就更靠前第一个就是最下面。接下来e就是sec的next也就是遍历到第三个了然后没有下一个while结束,for循环下一个数组元素。就这样循环往复遍历所有的元素转移到新数组。
  死环 问题就是在并发的情况下做这些扩容步骤出现的问题,通过上述演示单线程的一个扩容流程。假设现在两个线程同时对一个hashmap扩容。那么同时访问resize方法两个线程里面都创建了一个自己线程里的新数组,然后再执行transfer方法共同执行到while里的第一句next = e.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循环 得next = e.next。现在e.next = first而不是null,就出现循环了。处理完sec到时候e再引用first再循环一遍之后e才为空
  将sec移过去然后next为first(其实它的next本来就是first了因为开始那个线程已经改了因此前面next就不等于null,而是first)。导致接下来e 不是null,是first。然后再进while循环e =first,next=null。这时first就要插在sec上就是first的next改为sec。就形成了死循环
  总结这个问题产生就是因为头插法导致从旧数组到新数组的时候链表方向会反过来,再因为并发的问题开始读取的是first并且next是sec。但中途却被别的线程改了因为扩容next反过来。它还拿着之前的循环一次再取就是有问题了本来应该链表结束了为0,结果可以连到fisrt。  JDK8的优化resize 扩容优化 解决了resize时多线程死循环问题 引入了红黑树,目的是避免单条链表过长而影响查询效率
  关于resize优化,在1.7上面扩容源码里拿到元素重新类似取模运算h & (length-1)得到位置,实际上在扩容是原来的两倍,这个时候取模结果就是原来的位置或原来的位置加原数组长度就这两种情况。  //扩容前长度16 length-1   0000 0000 1111 hashCode1  1011 0010 0111  (111)3 hashCode2  0001 0101 0111  (111)3//扩容后长度32 length-1   0000 0001 1111 hashCode1  1011 0010 0111  (111)3 hashCode2  1101 0101 0111  (10111)19
  所以hashcode之前没用到的高一位要么是0要么是1。新数组容量是以前两倍高位多了个1,hashcode如果高一位是0和以前就没有区别,如果是1就多了1 一个原数组大小。从以前的111要么没有要么还是111要么是10111就是看那个高位所以我们只需要判断h & length的结果。就看那一个位就可以判断它的新下标是不变还是加上老数组容量  //扩容前长度16 length     0000 0001 0000 hashCode1  1011 0010 0111  (0000 0000)0 hashCode2  0001 0101 0111  (0001 0000)16//这段代码放在遍历原数组里面 if((h&length)=0){ 	当前遍历的下标i就是新数组下标 }else{ 	当前遍历的i+oldTable.length就是新下标 }
  1.8源码当中也就是这样去确定新的下标位置  Node loHead = null, loTail = null; Node hiHead = null, hiTail = null; Node 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; }
  在上述源码中,除了新坐标的计算外。内部定义的两组共四个指针(低头、低尾、高头、高尾)低高区分新下标是保持原样的还是增加的和上面提的一样不需要rehash,无论是低指针还是高指针都有两个头尾指针head、tail,标记头尾也是解决1.7当中由于体位倒置会出现的死循环问题

男篮输给澳大利亚虽然遗憾,但展现出来的精神面貌让人看到了希望看了昨天的比赛,感觉我们的比赛节奏太慢了,太软了,有队员体能,更多的是比赛规则不提倡对抗,一有对抗裁判就吹哨,很能抢戏,看看我们的队员在和澳大利亚的对抗中,挤出场外就找裁判,裁判示恭喜!步行者达成1换6交易,杜兰特要价出炉,湖人将追求戈登北京时间7月3日,NBA自由市场已经开启,其中菲尼克斯太阳是凯文杜兰特心仪下家,因为他与主教练蒙蒂威廉姆斯非常熟悉,克莱透露,输掉总决赛的时刻仍刺痛着我。要么夺冠要么一败涂地。就在央视今天直播!中国男篮VS澳大利亚,杜锋要赢球需用好3人少用2人2023年男篮世界杯亚太区预选赛第三窗口期的比赛,中国男篮迎来了第三轮比赛,对手是澳大利亚男篮,在此前,双方有过一次交手,最终,中国男篮以7分之差输给了对手,这场比赛中国男篮虽然是中国战澳大利亚末节仅得到3分,打破正式比赛单节得分纪录北京时间今日,男篮世预赛第三窗口期比赛中国队4871不敌澳大利亚,中国末节仅得到3分。中国男篮单节仅得3分打破中国男篮正式比赛单节最低分,此前为2018年中澳男篮对抗赛的4分和20何冰娶同桌当妻子,9个月就闪婚,婚后24年零绯闻2005年,大宋提刑官播出,演员何冰也因此大火。在演艺事业上,何冰并不算是一帆风顺,在蛰伏多年后才能大火。不过,和事业相比,何冰的感情生活却是极为顺利。何冰和妻子喝了一次茶,吃了一东亚杯国足征调名单基本确定,张琳芃吴曦张玉宁在列,归化驰援改用选拔队,抽调五名主力包括归化,剑指东亚杯2022年东亚足球锦标赛(东亚杯)将在北京时间7月19日开打,已经剩下半个月不到时间,中国男足也由之前定好的U23国家队改用国足选拔队参田华与初恋结婚生三子,晚年失去四位至亲,94岁还活跃在荧屏田华在电影白毛女(1950)中的影像谁也没有想到的是,当白毛女(1950)重新回归到大众视线中,在经过长达半个多世纪后,人们对这部电影的喜爱更是有过之而无不及。因为如今的观众从这部放不下包袱就别演丑角!这12位明星告诉你,什么叫敬业不糊弄观众现在越来越多流量明星不愿意去为了角色去装扮丑的形象,为了保持美美的形象,一般不会接带有丑角的戏!今天就让我们来盘点一下有哪些男艺人或女艺人扮演过哪些丑角色,他们为了体现出更好的影视男星减肥越减越丑?有人暴瘦30斤像老头,有人为了瘦差点送命随着娱乐圈的内卷愈发严重,筷子腿蚂蚁腰排骨胸,早已不是女明星们的专属名词,很多男明星也纷纷加入了减肥大军中。男明星们减肥,有的是因为角色需要,有的则是为了去油解腻,然而当他们减肥过浪姐成牺牲品?为捧披哥忽略姐姐,总导演怒而注销微博7月3日,乘风破浪的姐姐与披荆斩棘的哥哥同时登上热搜。起因是总导演吴梦知把节目组勾心斗角的故事搬到了公共平台,引发网友质疑节目组偏心不公平,认为对披哥的重视程度超过浪姐,不仅把浪姐幸福到万家告诉我们当配角出彩时,主角已不再重要一直稳坐收视宝座的梦华录没有剧集能将其拉下,但赵丽颖的幸福到万家上线后,瞬间将其拉下宝座。赵丽颖作为85花的流量担当,扛剧能力依旧不能小觑,赵丽颖此次出现农村题材的剧,可以说是一部
乔丹御用训练师评价他从不招募超级巨星与他并肩作战作为6次冠军,迈克尔乔丹在他的NBA职业生涯中取得了巨大的成功。虽然一路上他得到了斯科蒂皮蓬丹尼斯罗德曼和史蒂夫科尔等人的大量帮助,但他从来不需要出去寻求别人的帮助。正如他的体能教维金斯普尔拒绝降薪,勇士解体倒计时,美媒爆1换5交易北京时间6月23日,自由市场即将开启,各支球队已经跃跃欲试,开启了新一轮的挖人大战。比起其他球队在自由市场上的大浪淘沙,勇士的续约工作也将是如履薄冰,一旦处理不好,刚建立的王朝恐将选秀大丰收!湖人选到小普尔连签3人,威少或放弃4700万离队2022年选秀大会,湖人本来是没有选秀权的,他们的首轮签和次轮签都送出去了。但湖人有钱,他们用现金未来的1个次轮签,从魔术那得到了今年的35号签。最终湖人用35号签选中了有着小普尔一梦江湖今天是甜酷的异域风格!这次新时装打到了谁的心巴不知道大家有没有关注到一梦江湖刚刚公布的新时装?一起来夸一下,梦宝今年的衣服审美越来越高了最新公布的时装大千须弥是梦神山系列的最新款,这套时装融合了锦绣河山的意蕴,也暗含了芥子藏须司机是低等级地位?理想汽车创始人言论惹争议,断章取义商悟社张志雪6月23日报道网传的一段视频中,理想汽车创始人李想说司机在车里如果有老板的话,司机是一个低一等级,身份很低的人。这句话被人裁剪成短视频进行传播引发了争议和批评。对此,在2015年美国以学术会议之名设鸿门宴,我国芯片大佬被捕,至今未归第三次科技革命方兴未艾,谁把握住了尖端技术,就是拿下了未来国际竞争的主动权,而芯片制造即是当今社会最关注的领域之一。芯片在我们的生活中可以说是无处不在,手机电脑家电飞机等等产品中都旭辉630大裁员?到底什么情况。。近一周来,旭辉630大裁员的消息,在各个地产群疯传。起因是有位猎头称,旭辉为了8月底的外债节点,将进行大裁员,还煞有介事地给出了3个方案。此言一出,人心惶惶。在公众号后台,有不少读美国传来新消息,外媒台积电在准备迎接华为我们都知道,因为美国科技霸权,凡是只要使用了美国技术,哪怕只有千分之一万分之一,那也要受到美国的制约,如果要想供货华为,或者为华为提供服务,就必须要经过美国的同意。而台积电由于有很一块大屏带来全屋智能新操控小米智能家庭屏10评测智能家居其实已经到了可以谈论操控的时代。一套智能家居系统,小到智能插座墙壁开关,大到智能电视洗衣机,每个组件都有很多的控制功能。一些朋友的智能家居APP中,设备清单都好几页,设备几中年之人,看轻感情,你就赢了情感话题解读,带您走进更多的正向情感世界!我是你的情感解忧人,但你要懂得放过你自己!文风停夜泊原创抄袭必究年轻人无论男女,都容易在爱情里沉沦。随着年纪的增长,越是现实的当下,人们越小米三员工内部开撕?腾讯新闻架构调整?TME员工跟外包区别?2022年6月23日,星期四。这其实挺八卦的一件事,但笔者觉得还是有说一说的必要。2021年7月14日,小米一女同学向自己一位同事发微信消息称同事xx是海王,公司有个女孩给xx送吃