当前位置: 首页 > 后端技术 > Java

HashMap中的这些设计完美

时间:2023-04-01 14:45:50 Java

送你下面的java学习资料。获取方式见文末。1、HashMap构造函数HashMap一共为我们提供了三个构造函数来创建HashMap对象。1、无参构造函数publicHashMap():使用无参构造函数创建的hashmap对象,默认容量为16,默认加载因子为0.75。2.Argument-parameterconstructorpublicHashMap(intinitialCapacity,floatloadFactor):使用这个构造函数,我们可以指定hashmap的初始化容量和加载因子,但是hashmap的底层不一定会初始化到我们传递的容量in,但是会被初始化为大于等于传入值的最小的2次方。比如我们传入17,那么hashmap会被初始化为32(2^5)。那么hashmap是如何高效计算大于等于一个数的最小2次方呢?源码如下: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)?最大容量:n+1;}它的设计可以说是非常巧妙了。基本思想是如果一个二进制数的低位全为1,那么这个数+1一定是2的幂。我们来看一个例子:可以看到它的计算过程是:首先,减去1从我们指定的数字cap(之所以要减1是因为如果cap恰好是2的幂,也可以正确计算),然后将cap-1无符号右移1位、2位、4位,8位,和16位(和正好是31位),每次移位后和前面的数按位或运算,通过这样的运算,最后结果的低位全为1.然后加1最后的结果,你会得到2的幂。3.另一个参数化构造函数是参数化构造函数publicHashMap(intinitialCapacity)。此构造函数与前一个构造函数之间的唯一区别是无法指定负载因子。二、HashMap插入机制1、插入方法源码publicVput(Kkey,Vvalue){returnputVal(hash(key),key,value,false,true);}finalVputVal(inthash,Kkey,V值,booleanonlyIfAbsent,booleanevict){Node[]tab;节点p;诠释n,我;//初始化bucketarraytable,table被延迟直到插入新数据if((tab=table)==null||(n=tab.length)==0)n=(tab=resize()).length;//如果桶中不包含键值对节点引用,则表示当前如果数组下标下没有数据,则将新键值对节点的引用存入桶中if((p=tab[i=(n-1)&hash])==null)tab[i]=newNode(hash,key,value,null);else{节点e;Kk;//如果hash相等,equals方法返回true,说明key相同,此时可以直接替换value,并返回原值if(p.hash==hash&&hash&&((k=p.key)==key||(key!=null&&key.equals(k))))e=p;first//if如果一个节点是树节点,调用putTreeVal方法将当前值放入红黑树中elseif(pinstanceofTreeNode)e=((TreeNode)p).putTreeVal(this,tab,hash,key,value);else{//如果第一个节点不是树节点,说明它还是一个链表节点,然后开始遍历链表,把值存到链表合适的位置for(intbinCount=0;;++binCount){//如果遍历到链表的末尾,则创建一个链表节点,并将链表末尾的数据存入nullif((e==p.next){p.next=newNode(hash,key,value,null);还有判断)if(bincount>=treeify_threshold-1);//-1havetreshold-1);//-1havebreak;}//如果在链表中发现相同的key,直接替换ValueIF(e.hash==Hash&&((k=e.Key)==Key||(key!=Null&&Key.equals(k)))break;p=e;}//e!遍历到中间的时候会break,此时在链表中找到完全相等的key,在if块中,值的替换操作IF(E!=Null){//ExistingMappingforKeyVOldvalue=E.value;如果(!OnlyifabSEnt||Oldvalue==NULL)E.Value=ValueAccess(E);旧值;}}++MODCount;//添加值后,更新size,如果超过阈值,则上限(++size>threshold);(1)放一个k-v的时候,先调用hash()方法计算出key的hashcode,但是在hashmap中,并不是简单的调用key的hashcode来求一个hashcode,还要利用扰动functiontoreducehashconflicts源码如下:staticfinalinthash(Objectkey){inth;返回(键==空)?0:(h=key.hashCode())^(h>>>16);}从源码中可以看出,最终的哈希值是将原哈希码与原哈希码右移16位得到的值进行异或运算的结果。16正好是32的一半,所以hashmap将hashcode的高位移动到低位,然后通过异或运算将高位向低位扩散,从而减少哈希冲突。至于为什么可以减少冲突,可以看作者对hash方法的评论:Computeskey.hashCode()andspreads(XORs)hashhigherbitstolower。因为该表使用二次幂掩码,所以仅在当前掩码以上的位上有所不同的散列集将始终发生冲突。(已知的示例是在小表中保存连续整数的Float键集。)所以我们应用转换向下传播更高位的影响。在位扩展的速度、效用和质量之间存在权衡。因为许多常见的哈希集已经合理分布(因此不会从传播中获益),并且因为我们使用树来处理bins中的大量冲突,所以我们只是以最便宜的方式对一些移位的位进行XOR以减少系统损失,以及合并最高位的影响,否则由于表边界而永远不会在索引计算中使用这些位。原因是:由于hashmap计算的是桶下标,计算方式是hash&n-1,n是2的幂,所以hash&n-1只是取出hash的低位,比如n为16,那么hash&n-1取出hash的低四位,那么如果多个hash的低四位正好相等,就会导致总是碰撞(conflict),即使hash不一样,也将高位分散到低位,让高位也可以参与计算,从而减少冲突,让数据存储更加hashable。(2)计算hash后,调用putVal方法存储key-value。在putVal方法中,首先要判断表是否已经初始化(因为hashmap是惰性初始化的,创建对象的时候不会初始化表)。如果表还没有初始化,则使用resize方法扩容。如果((tab=table)==null||(n=tab.length)==0)n=(tab=resize()).length;(3)通过(n-1)&哈希桶下标计算当前key,如果当前表中当前下标没有存储数据,则创建一个链表节点,直接将当前k-v存储在下标位置。如果((p=tab[i=(n-1)&hash])==null)tab[i]=newNode(hash,key,value,null);(4)如果表下标处已经有数据,则先判断当前key是否与下标处存储的key完全相等,若相等则直接替换值,返回原值,否则继续遍历链表或者存入红黑树。如果(p.hash==hash&&((k=p.key)==key||(key!=null&&key.equals(k))))e=p;(5)当前下标处的节点如果是树节点,则直接存入红黑树elseif(pinstanceofTreeNode)e=((TreeNode)p).putTreeVal(这个,制表符,散列,键,值);(6)if如果不是红黑树,则遍历链表。如果在遍历链表的过程中发现了等键,则替换该值。如果没有等键,则存储链表尾部的节点(jdk8使用尾插入方式),并检查当前链表中的节点树是否超过阈值8,如果超过8,则转换链表通过调用treeifyBin方法将列表转化为红黑树。对于(intbinCount=0;;++binCount){if((e=p.next)==null){p.next=newNode(hash,key,value,null);if(binCount>=TREEIFY_THRESHOLD-1)//-1对于第一个treeifyBin(tab,hash);休息;}}if(e.hash==hash&&((k=e.key)==key||))))中断;p=e;}(7)数据存入后,需要判断当前hashmap的大小是否超过扩容阈值Cap*load_fact,如果大于阈值,则调用resize()方法进行扩容。f(++size>threshold)调整大小();HashMap扩容后的容量是原来的两倍。基本机制是创建一个容量增加一倍的表,然后将数据转移到新的哈希表中,并返回新的哈希表。与jdk1.7不同的是jdk1.8中的multi-dumping进行了优化,不再需要重新计算bucket下标。其实现源码如下:从源码中我们可以看出,如果一个keyhash与原始容量oldCap按位AND运算结果为0,扩容前的bucket下标等于bucket展开后的下标。否则扩容后的桶下标为原下标加上oldCap。使用的基本原理总结如下:1.如果一个数m和2的n次方不等于0,则有:m&(n2-1)=m&(n-1)+n理解:一个数n的2次方。在二进制中,只有一位为1(假设第k位为1),其他位全为0。如果一个数m和n按位与运算的结果为0,则表明二进制m的第k位必须为0,那么m的前n位和前n-1位所代表的值必然相等。2、如果一个数m和2的n次方按位与运算等于0,则:m&(n2-1)=m&(n-1)理解:2的n次方,在二进制中,只有一个bit为1(假设第k位为1),其他位全为0。如果一个数m和n按位与运算的结果不为0,则表示m的二进制第k位为积极的。为1,则m的前n位所代表的值与前n-1位所代表的值之差恰好是第k位1所代表的数,而数二恰好是n。原理图: