前几天,H同学和我聊了聊谷歌的面试经历。没想到谷歌之后也问了收藏,感觉又要整理一波收藏相关的问题了。阅读文章前,可以先看看以下高频测试站点。如果觉得没有什么问题,可以直接跳过本文,不浪费时间。newHashMap()和newHashMap(intinitialCapacity)的具体区别是什么?当HashMap中的数据数超过多少时,容量会扩容吗?HashMap中的链表结构什么时候转成红黑树?会转换吗?Red什么时候将黑色的树结构转换回链表?再说说HashMap的懒加载?负载因子的作用?特征分析为了厘清一个概念,本文引入了竹篓和鸡蛋的概念。缓存的数据是鸡蛋。节点是篮子,HashMap底层是篮子数组。每个篮子都是一个链表或者红黑树结构。每个篮子还可以放多个鸡蛋,所以实际上可以把它看成一个二维数组。这里的二维数组就是链表或者红黑树。至于竹篓的结构是链表还是红黑树,要看竹篓里鸡蛋的个数,链表和红黑树是可以转换的。通过散列函数将鸡蛋定位到表中特定的竹筐中,提高查询速度。用于存储数据的底层数组也称为哈希表。所谓散列函数,简单来说就是将一个无限集(在HashMap中,key值是一个无限集)均匀分布在一个有限集(我们定义的哈希表Capacity,比如一个长度为16)源码分析成员变量和常量staticfinalintDEFAULT_INITIAL_CAPACITY=1<<4;//默认初始容量,估计所有的竹篮可以装16个鸡蛋staticfinalintMAXIMUM_CAPACITY=1<<30;//最大容量,可以装多少个鸡蛋放在所有的竹篮子里staticfinalintfloatDEFAULT_LOAD_FACTOR=0.75f;//默认的负载因子用来装逼,一般在应用里讨论bb的时候都会提到staticfinalintTREEIFY_THRESHOLD=8;//单个竹篮子的鸡蛋数量超过8转red-blacktreestaticfinalintUNTREEIFY_THRESHOLD=6;//如果单个竹筐的鸡蛋个数小于6,则转移到链表中staticfinalintMIN_TREEIFY_CAPACITY=64;//如果竹筐的个数为小于64,先扩容,链表不会变红BlacktreetransientNode[]table;//哈希表数组,存储数据的地方,每个竹篮结构可能是一个链表list或者红黑树,总是2的幂transientSet>entrySet;//具体元素的集合,可以用来遍历Maptransientintsize;//鸡蛋总数transientintmodCount;//插入和删除元素,modCount++,用于记录变化次数intthreshold;//容量阈值,所有竹篮子允许缓存的鸡蛋个数。如果超过这个值,就需要扩容。默认为0。了解扩展在未来将如何变化非常重要。笔试或者面试需要记住几个死记硬背的点,就是:hashmap默认初始容量为16,负载因子为0.75,链表长度超过8,会转为红黑树,而小于6则转为链表,容量小于64则先扩容,不将链表转为红黑树。看一下构造方法。HashMap有四种构造方法。这里只列出常用的两种。第一个是默认的构造方法。我们不指定默认值。所有的竹篮总共可以装16个鸡蛋。二是指定初始容量,这也是我们建议大家根据自己的需要来设置大小,避免后续resize扩展的开销。如果在日常开发中知道size,我们会采用第二种方式来避免resize扩展的开销。新手开发者往往直接使用第一种方法。一般我review代码,看到了,直接回调改,然后告诉他们为什么。想一想,如果你已经知道map容器会容纳100个元素,而你没有指定初始容量,那么会导致第一次putdata的时候,因为第一次扩容没有初始容量,计算出的容量阈值为默认大小16,16*负载因子0.75f为12,然后当你继续放超过12时,会继续扩容,第二次扩容后的容量阈值为24,重复刚才的过程,直到容量阈值大于100,如果指定默认大小,第一次扩容就够了,后面不需要再扩容那么多,可以节省性能;还要注意命名,最好直接用Map,比如xxxMap,这是规范的。扩容,我们先回顾一下上面的成员变量threshold"intthreshold;//容量阈值,所有篮子允许放的鸡蛋数量"ok,我们继续说扩容,扩容可以分为多种情况。在构造方法中可以看到成员变量threshold容量threshold并没有被初始化。在阈值赋值反了的地方,可以看到放第一个元素的时候会调用resize方法。该方法计算了容量阈值。方法是:DEFAULT_LOAD_FACTOR*DEFAULT_INITIAL_CAPACITY,即默认负载因子*默认容量,即0.75f*16,即12。如果指定初始容量,可以看到计算公式为初始容量阈值是:这样一坨东西是什么东西?其实,它只是一种算法,看不懂也没关系。毕竟我也看不懂。记住它。这个算法实际上返回的是一个大于输入参数和2的最接近整数次方的数。比如初始容量设置为10,那么算法返回16。但是,此时数组实际上并没有分配.可以看到resize方法其实是在数组put的时候分配的,也就是懒加载。第二次扩容最终会导致resize方法,每次扩容都会使原来的容量增加一倍。我们可以看看调整大小的方法finalNode[]resize(){Node[]oldTab=table;intoldCap=(oldTab==null)?0:oldTab.length;intoldThr=threshold;intnewCap,newThr=0;if(oldCap>0){//如果竹篓数量大于最大容量,则设置容量阈值为intif(oldCap>=MAXIMUM_CAPACITY){threshold=Integer.MAX_VALUE;returnoldTab;}//否则新阈值是旧阈值的两倍elseif((newCap=oldCap<<1)=DEFAULT_INITIAL_CAPACITY)newThr=oldThr<<1;}elseif(oldThr>0)newCap=oldThr;else{//只有默认的无参数构造方法才会走到这个分支,阈值是默认算法,capacity*0.75newCap=DEFAULT_INITIAL_CAPACITY;newThr=(int)(DEFAULT_LOAD_FACTOR*DEFAULT_INITIAL_CAPACITY);}if(newThr==0){floatft=(float)newCap*loadFactor;newThr=(newCap[]newTab=(Node[])newNode[newCap]。;table=newTab;if(oldTab!=null){//轮询操作,重新散列所有元素r(intj=0;je;if((e=oldTab[j])!=null){oldTab[j]=null;if(e.next==null)newTab[e.hash&(newCap-1)]=e;elseif(einstanceofTreeNode)//竹篓里的鸡蛋数据重新哈希,红黑树转入链表。内部处理主要是当竹篓里的鸡蛋个数小于等于6时,会恢复树结构为链表((TreeNode)e).split(this,newTab,j,oldCap);else{//preserveorder//简称:链表的元素是重哈希}}}}returnnewTab;}HashMap扩展分为三种:使用默认构造方法初始化HashMap。从上一篇文章我们可以知道HashMap在初始化的时候thresholdcapacity的阈值是0,默认值DEFAULT_INITIAL_CAPACITY是16,DEFAULT_LOAD_FACTOR是0.75f,所以第一次放数据的时候会进行扩容.扩容后的容量阈值为DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR=12;指定初始容量的构造方法初始化了HashMap,可以看到在初始化的时候是通过tableSizeFor计算的。即返回大于输入参数和最接近的2的整数次幂的数;当HashMap不是第一次扩容时,那么每次扩容的容量和容量阈值threshold都是原来哈希计算的两倍HashCode是什么?我们都知道无非就是确定对象在HashMap中的存储地址。目的也很简单。为了速度,好像除了那方面,就是越快越好,比如赚钱。元素插入,我们回顾一下上面的成员变量表“itransientNode[]table;//哈希表数组,存放数据的地方,每个篮子结构可能是一个链表,也可能是一个红黑树。是2倍的幂"finalVputVal(inthash,Kkey,Vvalue,booleanonlyIfAbsent,booleanevict){Node[]tab;Nodep;intn,i;//这里如果找到动态数组为null将初始化数组。if((tab=table)==null||(n=tab.length)==0)//第一次放入值时,这里会初始化数组,扩容resizemethodn=(tab=resize()).length;//通过hash发现要放的egg的数组位置为null,说明没有hash冲突,所以直接放egghereif((p=tab[i=(n-1)&hash])==null)tab[i]=newNode(hash,key,value,null);else{//如果已经有蛋在positiontobeplacedNodee;Kk;//判断竹篮子里第一个鸡蛋和新元素的key和hash值是否完全一样。如果是这样,您无需查看代码即可知道如何覆盖。覆盖的逻辑在后面if(p.hash==hash&&((k=p.key)==key||(key!=null&&key.equals(k))))e=p;//确认是否是树的解点elseif(pinstanceofTreeNode)//如果是,按照红黑树的方法放到竹筐里e=((TreeNode)p).putTreeVal(this,tab,hash,key,value);else{//说明不是,是一个list,按照list的方法放入for(intbinCount=0;;++binCount){//往下找,直到找到空位置if((e=p.next)==null){p.next=newNode(hash,key,value,null);//判断链表长度是否大于8,这里减去后实际为7,判断的是binCount,但是因为插入了一个新节点,所以实际上是8if(binCount>=TREEIFY_THRESHOLD-1)//链表转为红黑树,所以记住大于8的时候会转成红黑树treeifyBin(tab,hash);break;}//已经找到和key相同的hash值,那么直接break,不用找了,还有稍后覆盖if(e.hash==hash&&((k=e.key)==key||(key!=null&&key.equals(k))))break;p=e;}}//这里覆盖操作,新值和旧值的key完全一致,覆盖覆盖操作if(e!=null){VoldValue=e.value;if(!onlyIfAbsent||oldValue==null)e.value=value;//访问后回调afterNodeAccess(e);returnoldValue;}}++modCount;//当map中所有鸡蛋的个数大于容量阈值时,扩大容量。第二次扩展就是上面说的,也就是两次if(++size>threshold)resize();afterNodeInsertion(evict);returnnull;}finalVputVal(inthash,Kkey,Vvalue,booleanonlyIfAbsent,booleanevict){Node[]tab;Nodep;intn,i;//这里如果发现动态数组为null,数组将被初始化if((tab=table)==null||(n=tab.length)==0)//第一次放入值时,这里会初始化数组,扩容resizemethodn=(tab=resize()).length;//通过hash发现要放的egg的数组位置为null,说明没有hash冲突,所以直接放egghereif((p=tab[i=(n-1)&hash])==null)tab[i]=newNode(hash,key,value,null);else{//如果已经有蛋在positiontobeplacedNodee;Kk;//判断竹篮子里第一个鸡蛋和新元素的key和hash值是否完全一样。如果是这样,您无需查看代码即可知道如何覆盖。覆盖的逻辑在后面if(p.hash==hash&&((k=p.key)==key||(key!=null&&key.equals(k))))e=p;//确认是否是树的解点elseif(pinstanceofTreeNode)//如果是,按照红黑树的方法放到竹筐里e=((TreeNode)p).putTreeVal(this,tab,hash,key,value);else{//说明不是,是一个list,按照list的方法放入for(intbinCount=0;;++binCount){//往下找,直到找到空位置if((e=p.next)==null){p.next=newNode(hash,key,value,null);//判断链表长度是否大于8,这里减去后实际为7,判断的是binCount,但是因为插入了一个新节点,所以实际上是8if(binCount>=TREEIFY_THRESHOLD-1)//链表转为红黑树,所以记住大于8的时候会转成红黑树treeifyBin(tab,hash);break;}//已经找到和key相同的hash值,那么直接break,不用找了,还有稍后覆盖if(e.hash==hash&&((k=e.key)==key||(key!=null&&key.equals(k))))break;p=e;}}//这里覆盖操作,新值和旧值的key完全一致,覆盖覆盖操作if(e!=null){VoldValue=e.value;if(!onlyIfAbsent||oldValue==null)e.value=value;//访问后回调afterNodeAccess(e);returnoldValue;}}++modCount;//当map中所有鸡蛋的个数大于容量阈值时,扩大容量。第二次扩展就是上面说的,也就是两次if(++size>threshold)resize();afterNodeInsertion(evict);returnnull;}finalvoidtreeifyBin(Node[]tab,inthash){intn,index;Nodee;//当tab为空或竹篓数小于64时,先展开容量而不是将列表链接到红黑树if(tab==null||(n=tab.length)hd=null,tl=null;do{TreeNodep=replacementTreeNode(e,null);if(tl==null)hd=p;else{p.prev=tl;tl.next=p;}tl=p;}while((e=e.next)!=null);if((tab[index]=hd)!=null)hd.treeify(tab);}}put的整体流程总结如下:列举几点需要注意的是,即:HashMap中缓存数据的数组表,我们可以看到初始化的时候默认为null,第一次放数据的时候才初始化,也是so-称为延迟加载。记住,不要每次一提到HashMap的懒加载机制就一头雾水。HashMap中单个篮子存放的鸡蛋个数大于8个,当篮子个数大于64个时,链表会转为红黑树,否则扩容。每次放数据,当map中存储的所有鸡蛋数量大于容量阈值时,扩容,先放数据,再扩容。元素查询finalNodegetNode(inthash,Objectkey){Node[]tab;Nodefirst,e;intn;Kk;if((tab=table)!=null&&(n=tab.length)>0&&(first=tab[(n-1)&hash])!=null){//如果计算出的hash值与第一个节点的key值相同,则直接返回if(first.hash==hash&&//alwayscheckfirstnode((k=first.key)==key||(key!=null&&key.equals(k))))returnfirst;if((e=first.next)!=null){//ifIf(firstinstanceofTreeNode)return((TreeNode)first).getTreeNode(hash,key);do{//如果hash值相同但key不同,则遍历链表找到相同的key返回if(e.hash==hash&&((k=e.key)==key||(key!=null&&key.equals(k))))returne;}while((e=e.next)!=null);}}returnnull;}看这里,总结过程就是:根据basket数组找到basket到hash值,判断头节点的key值与当前key相同,如果篮子是TreeNode节点,则直接返回,以红黑树方式查找,如果不是,则循环链表直到找到相同key对应的元素,删除finalNoderemoveNode(inthash,Objectkey,Objectvalue,booleanmatchValue,booleanmovable){Node[]tab;Nodep;intn,index;if((tab=table)!=null&&(n=tab.length)>0&&(p=tab[index=(n-1)&hash])!=null){Nodenode=null,e;Kk;Vv;if(p.hash==hash&&((k=p.key)==key||(key!=null&&key.equals(k)))node=p;elseif((e=p.next)!=null){//红黑树的查找方式if(pinstanceofTreeNode)node=((TreeNode)p).getTreeNode(hash,key);else{//链表遍历查找方法do{if(e.hash==hash&&((k=e.key)==key||(key!=null&&key.equals(k))){node=e;break;}p=e;}while((e=e.next)!=null);}}//删除nodeif(node!=null&&(!matchValue||(v=node.value)==value||(value!=null&&value.equals(v)))){//如果node是红黑树节点,使用红黑树删除方法if(nodeinstanceofTreeNode)((TreeNode)node).removeTreeNode(this,tab,movable);//如果是链表且node为头节点,则直接为当前数组下标元素replacedbynextelseif(node==p)tab[index]=node.next;else//如何删除链表的非头部元素p.next=node.next;++modCount;--size;afterNodeRemoval(node);returnnode;}}returnnull;}删除操作很简单,先根据hash找到对应的egg,然后根据不同类型的节点删除。没有什么需要注意的。如果有,说明如果是链表的表头,需要指定下一个节点作为表头。本文转载自微信公众号“粥雪”,可关注下方二维码。转载本文请联系粥夏雪公众号。