首页 > 编程知识 正文

hashtable底层实现原理,网页制作源码

时间:2023-05-04 14:51:34 阅读:23016 作者:1166

一. HashMap构造函数

HashMap具有三个用于创建HashMap对象的构造函数。

1 .无参数构造函数public HashMap () :使用无参数构造函数创建的hashmap对象的默认容量为16,默认加载因子为0.75。

2 .公共hashmap (intinitial capacity,浮动加载因子) :可以使用此构造函数指定hashmap的初始化容量和负载因子,但在hashmap下,这不一定是传入的源代码如下:

saticfinalinttablesizefor (int cap ) { int n=cap - 1; n |=n 1; n |=n 2; n |=n 4; n |=n 8; n |=n 16; 返回(n0 )? 1:(n=maximum_capacity )? MAXIMUM_CAPACITY : n 1; }那个设计可以说很巧妙。 其基本思想以如果一个二进制数低位全是1,那么这个数+1则肯定是一个2的幂次方数为例:

这个计算过程首先是从我们指定的几个cap中减去1 (减去1的原因是,如果cap正好是2的乘方,就可以准确计算),然后将cap-1分别无符号地向右1位、2位、4位、8位、16位(加起来正好是36位)

3 .另一个带参数的构造函数是带参数的构造函数公共散列(intinitial capacity,ishmap ),该构造函数与上一个构造函数唯一不同的是不能指定加载因子。

二. HashMap插入机制

1 .插入方法源

publicvput(kkey,V value ) returnputval ) hash ) key,key,value,false,true ); }finalvputval(inthash,K key,V value,boolean onlyIfAbsent,boolean evict ) { NodeK,V[] tab; NodeK,V p; int n,I; //桶数组table初始化,table延迟到插入新数据,然后初始化if((tab=table )==null|| (tab.length )=0) n=(tab=resize ) 如果指示当前数组下标下不存在数据,则只要将新键值对节点的引用保存到时段中,if((p=tab(I=(n-1 ) hash]] ) null (tab ) I )=newnode ) k; 如果hash相等,而equals方法返回true,则表示key相同。 在这种情况下,只需直接替换value即可,可以使用原始值if(p.hash==hash(((k=p.key )==key|(key!=nullkey.equals(k ) ) (e=p; //如果第一个节点是树节点,则调用putTreeVal方法,将当前值添加到红色黑色树中elseif(pinstanceoftreenode ) e=((treenodek,v ) p ).putTreeVal 如果tab )中包含的else { //第一个节点不是树节点,则表明它是链表节点,遍历链表,并将值存储在链表的相应位置for(intbincount=0)。 如果遍历到bincount(//链接的末尾,请创建链表节点,并将数据存储在链表的最后一个if((e=p.next )==null ) )中。

p.next = newNode(hash, key, value, null); //判断链表中节点树是否超多了阈值8,如果超过了则将链表转换为红黑树(当然不一定会转换,treeifyBin方法中还有判断) if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //如果在链表中找到,完全相同的key,则直接替换value if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //e!=null说明只是遍历到中间就break了,该种情况就是在链表中找到了完全相等的key,该if块中就是对value的替换操作 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //加入value之后,更新size,如果超过阈值,则进行扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }

2.插入流程图

(1)在put一个k-v时,首先调用hash()方法来计算key的hashcode,而在hashmap中并不是简单的调用key的hashcode求出一个哈希码,还用到了扰动函数来降低哈希冲突。源码如下:

static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }

从源码中可以看到,最终的哈希值是将tydggx码和tydggx码右移16位得到的值进行异或运算的结果。 16正好是32的一半,因此hashmap是将hashcode的高位移动到了低位,再通过异或运算将高位散播的低位,从而降低哈希冲突。至于为什么能够降低冲突呢,我们可以看看作者对hash方法的注释:

Computes key.hashCode() and spreads (XORs) higher bits of hash to lower. Because the table uses power-of-two masking, sets of hashes that vary only in bits above the current mask will always collide. (Among known examples are sets of Float keys holding consecutive whole numbers in small tables.) So we apply a transform that spreads the impact of higher bits downward. There is a tradeoff between speed, utility, and quality of bit-spreading. Because many common sets of hashes are already reasonably distributed (so don’t benefit from spreading), and because we use trees to handle large sets of collisions in bins, we just XOR some shifted bits in the cheapest possible way to reduce systematic lossage, as well as to incorporate impact of the highest bits that would otherwise never be used in index calculations because of table bounds.

从注释中我们可以得出,作者进行高位向低位散播的原因是:由于hashmap在计算bucket下标时,计算方法为hash&n-1,n是一个2的幂次方数,因此hash&n-1正好取出了hash的低位,比如n是16,那么hash&n-1取出的是hash的低四位,那么如果多个hash的低四位正好完全相等,这就导致了always collide(冲突),即使hash不同。因此将高位向低位散播,让高位也参与到计算中,从而降低冲突,让数据存储的更加散列。

(2)在计算出hash之后之后,调用putVal方法进行key-value的存储操作。在putVal方法中首先需要判断table是否被初始化了(因为hashmap是延迟初始化的,并不会在创建对象的时候初始化table),如果table还没有初始化,则通过resize方法进行扩容。

if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;

(3)通过(n-1)&hash计算出当前key所在的bucket下标,如果当前table中当前下标中还没有存储数据,则创建一个链表节点直接将当前k-v存储在该下标的位置。

if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);

(4)如果table下标处已经存在数据,则首先判断当前key是否和下标处存储的key完全相等,如果相等则直接替换value,并将原有value返回,否则继续遍历链表或者存储到红黑树。

if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k)))) e = p;

(5)当前下标处的节点是树节点,则直接存储到红黑树中

else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

(6)如果不是红黑树,则遍历链表,如果在遍历链表的过程中,找到相等的key,则替换value,如果没有相等的key,就将节点存储到链表尾部(jdk8中采用的是尾插法),并检查当前链表中的节点树是否超过了阈值8,如果超过了8,则通过调用treeifyBin方法将链表转化为红黑树。

for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; }

(7)将数据存储完成之后,需要判断当前hashmap的大小是否超过扩容阈值Cap*load_fact,如果大于阈值,则调用resize()方法进行扩容。

f (++size > threshold) resize();

HashMap在扩容后的容量为原容量的2倍,起基本机制是创建一个2倍容量的table,然后将数据转存到新的散列表中,并返回新的散列表。和jdk1.7中不同的是,jdk1.8中多转存进行了优化,可以不再需要重新计算bucket下标,其实现源码如下:

从源码中我们可以看出,如果一个key hash和原容量oldCap按位与运算结果为0,则扩容前的bucket下标和扩容后的bucket下标相等,否则扩容后的bucket下标是原下标加上oldCap。使用的基本原理总结如下:

1、如果一个数m和一个2的幂次方数n进行按位与运算不等于0,则有:m&(n2-1)=m&(n-1)+n
理解:一个2的幂次方数n,在二进制中只有一位为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,在二进制中只有一位为1(假设第k位是1),其他位均为0,那个如果一个数m和n进行按位与运算结果不为0的话,则说明m的二进制第k位肯定为1,那么m的前n位和前n-1位所表示的值的差恰好是第k位上的1所表示的数,二这个数正好是n。

原理图:

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。