Architecture前几天,一个小伙子向我抱怨。面试准备了好久,没想到问了一个HashMap就结束了。之后,他大致了解了面试流程。其实前几年,对于HashMap,大家问的都是比较简单的问题。随着大家水平的提升,这么简单的问题已经拦不住大家了。然后开始问一些经常被忽略但比较关键的小细节,开始往上卷,比如创建HashMap的时候需要指定容量吗?如果要指定,多少合适?为什么?如果我有100个具有不同hashcode的元素,它需要放在HashMap中。初始值设为100合适吗?是否要设置HashMap的初始容量?系统中的扩容机制决定了每次扩容都需要重建哈希表,对性能影响很大。我们可以看一下resize()的代码:无效的)?0:旧标签长度;intoldThr=threshold;//在默认构造函数的情况下为0intnewCap,newThr=0;if(oldCap>0){//表已经扩容//当前表容量大于最大值返回当前表if(oldCap>=MAXIMUM_CAPACITY){threshold=Integer.MAX_VALUE;返回旧标签;}elseif((newCap=oldCap<<1)=DEFAULT_INITIAL_CAPACITY)//表的容量乘以2,阈值也乘以2newThr=oldThr<<1;//doublethreshold}elseif(oldThr>0)//初始容量放在threshold中//当使用带有初始容量的构造函数时,表容量被初始化获得的阈值newCap=oldThr;else{//在默认构造函数下扩展//使用默认值的零初始阈值符号newCap=DEFAULT_INITIAL_CAPACITY;newThr=(int)(DEFAULT_LOAD_FACTOR*DEFAULT_INITIAL_CAPACITY);}if(newThr==0){//这里使用初始容量的构造函数展开floatft=(float)newCap*loadFactor;newThr=(newCap[]newTab=(Node[])newNode[newCap];表=新标签;if(oldTab!=null){for(intj=0;je;if((e=oldTab[j])!=null){//helpgcoldTab[j]=null;if(e.next==null)//当前索引没有hash冲突,直接对2取模,即移位操作hash&(2^n-1)//扩容是根据2的次方,所以newCap=2^nnewTab[e.hash&(newCap-1)]=e;elseif(einstanceofHashMap.TreeNode)//当前索引对应的节点是红黑树。算法,所以不做详细解释,当树的数量小于等于UNTREEIFY_THRESHOLD时,会转为链表((HashMap.TreeNode)e).split(this,newTab,j,老帽);else{//preserveorder//将当前索引对应的链表分成两个链表,减少扩容时的迁移量HashMap.NodeloHead=null,loTail=null;HashMap.NodehiHead=null,hiTail=null;HashMap.Node下一个;做{next=e.next;if((e.hash&oldCap)==0){//扩容后不需要移动的链表if(loTail==null)loHead=e;别的loTail.next=e;loTail=e;}else{//展开后需要移动的链表if(hiTail==null)hiHead=e;否则hiTail.next=e;高尾=e;}}while((e=next)!=null);if(loTail!=null){//帮助gcloTail.next=null;newTab[j]=loHead;}if(hiTail!=null){//帮助gchiTail.next=null;//扩展长度为当前索引位置+旧容量newTab[j+oldCap]=hiHead;}}}}}返回新标签;那么创建的时候指定多少合适呢?设置成多少?如果是这样,我会问你这个吗?因为,当我们使用HashMap(intinitialCapacity)来初始化capacity时,HashMap不会使用我们直接传入的initialCapacity作为初始容量。staticfinalinttableSizeFor(intcap){intn=cap-1;n|=n>>>1;n|=n>>>2;n|=n>>>4;n|=n>>>8;n|=n>>>16;返回(n<0)?1:(n>=MAXIMUM_CAPACITY)?MAXIMUM_CAPACITY:n+1;}所以我们在初始化的时候:newHashMap<>(cap)JDK其实就是求大于用户传入值的2的1次方。那么我们可以直接初始化为2的次方。那么也是不正确的,因为你忽略了一个因素:loadFactor。loadFactor是负载因子。当HashMap中的元素个数(size)超过threshold=loadFactor*capacity时,会扩容。也就是说,如果我们想在HashMap中保存7个元素,我们将默认值设置为7后,JDK会默认处理为8个。我们期望的是HashMap在PUT的时候不会扩容,这样对性能没有影响,但事实并非如此。这个HashMap会在元素个数达到8*0.75=6时扩容一次。publicstaticvoidmain(String[]args)throwsNoSuchFieldException,IllegalAccessException{HashMaphashMap=newHashMap<>(7);类clazz=HashMap.class;//threshold是hashmap对象中的一个私有变量,如果hashmap的大小超过这个值,就会扩容。这个是通过反射得到的Fieldfield=clazz.getDeclaredField("threshold");field.setAccessible(true);intthreshold_begin=(int)field.get(hashMap);System.out.println("初始化大小:"+threshold_begin);整数阈值=0;对于(inti=0;i<8;i++){hashMap.put(String.valueOf(i),0);if((int)field.get(hashMap)!=threshold){threshold=(int)field.get(hashMap);System.out.println("起始容量:"+threshold);//默认加载因子为0.75,即实际容量为/0.75System.out.println("aftercapacity:"+(int)field.get(hashMap)/0.75);System.out.println("==================");那么我们应该怎么设置呢?其实我们可以参考JDK8中putAll方法中的实现:比如我们打算往HashMap中放入7个元素,通过expectedSize/0.75F+1.0F计算,7/0.75+1=10,之后10经过JDK处理,会设置为16,这样就大大减少了扩容的机会。但是会牺牲一些内存。其实在guava中已经实现了这个算法:Mapmap=Maps.newHashMapWithExpectedSize(7);publicstaticHashMapnewHashMapWithExpectedSize(intexpectedSize){returnnewHashMap(capacity(expectedSize));}staticintcapacity(intexpectedSize){如果(expectedSize<3){CollectPreconditions.checkNonnegative(expectedSize,"预期大小");返回expectedSize+1;}else{返回expectedSize<1073741824?(int)((float)expectedSize/0.75F+1.0F):2147483647;}}其实这是一种用内存换取性能的方法,但是在当前环境下,低算力成本和高网络带宽使得大部分开发者在遇到性能问题时,可以通过增加CPU来解决问题和服务器的内存不假思索。实际上,大的优化是通过小的优化积累起来的。但是现在来问这些东西,大部分都是为了筛选。金三银四祝大家面试顺利。