标签:hash冲突 源码 性能 一个 style 位运算 put hashmap 元素
底层最核心的数据结构是数组,我们构造一个map,往里面放入数据,比如我们放入key为张三,value为测试数据,对张三计算出一个hash值,根据这个hash值对数组进行取模,就会定位到数组里的一个元素中去
还是以map.put("张三","测试数据")为例
对"张三"这个key计算它的hash值,会有一定的优化
//JDk1.8以后的HashMap里面的一段源码
static final int hash(Object key){ int h; return(key == null) ?0:(h=key.hashCode())^(h>>>16); }
比如说:有一个key的hash值
1111 1111 1111 1111 1111 1010 0111 1100
0000 0000 0000 0000 1111 1111 1111 1111(右移16位:n>>>16)
异或结果:1111 1111 1111 1111 0000 0101 1000 0011
结果是二进制的表示,转换成一个int值,也就是代码里面的h,这个值就是hash值
我们可以看出,其实 (h=key.hashCode())^(h>>>16) 是把hash值的高16位和低16位进行了一个运算
(n-1)& hash ->数组里的一个位置 (n代表数组长度,此处n为16)
1111 1111 1111 1111 1111 1010 0111 1100
0000 0000 0000 0000 0000 0000 0000 1111 (n-1:也就是15的二进制)
1111 1111 1111 1111 0000 0101 1000 0011 (优化后的hash值)
取模运算,他是性能比较差一些,为了优化这个数组寻址的过程
hash&(n-1) ->效果是跟hash对n取模,效果是一样的,但是与运算的性能要比hash对n取模要高很多。那为什么要高很多了?这是一个数学问题,数组的长度会一直是2的n次方,只要他保持数组长度是2的n次方,hash对n取模的效果就和hash & (n-1)一样的,而后者的性能更高
如果我们拿没有经过优化的hash值进行与运算,其实高16位之间的运算是可以忽略的,核心点在于低16位的运算,hash值的高16位就没有参与到运算里面来
假设有两个hash值
1111 1111 1111 1111 1111 010 0111 1100
1111 1111 1111 1111 1110 1010 0111 1100
它们两个的低16位是一样的,高16位不同,由于高16位的运算参与可以忽略不计,所以就会造成困扰,那么经过优化后得到的hash值低16位就会有很大不同
总结
hash算法的优化:对每个hash值,在他的低16位中,让高低16位进行了异或,让他的低16为同时保持了高低16位的特征,尽量避免一些hash值后续出现冲突,大家可能会进入数组的同一个位置
寻址算法的优化:用与运算替代取模,提升性能
hash冲突问题,通过链表加红黑树问题来解决
两个key,多个key,算出来的hash值与n-1运算后,定位出来的数组的位置还是一样的,这就是hash碰撞,那怎么解决了?
会在这个位置挂一个链表,这个链表里面放入多个元素,让多个key-value对,同时放在数组的一个位置里
我们get取的时候,如果定位到数组里发现这个位置挂了一个链表,此时遍历链表,从里面找到自己要的那个key-value对
如果只是链表的话,链表很长,那么遍历链表的时候,性能就会比较差,O(n);
所以有了一个优化,当链表的长度达到了一定的长度后,会把链表转换成红黑树,遍历一颗红黑树找一个元素,此时O(logn),性能会比链表高一些
底层是一个数组,当这个数组满了以后,他就会自动进行默认二倍扩容,变成一个更大的数组,比如数组16位,就会扩容到32位
数组变大以后会进行一个rehash,重新把数组定位到32位数组里面的位置里面
以一个数组长度为16的Map举例
n-1 0000 0000 0000 0000 0000 0000 0000 1111
hash1 1111 1111 1111 1111 0000 1111 0000 0101
&结果 0000 0000 0000 0000 0000 0000 0000 0101 = 5(index = 5的位置)
n-1 0000 0000 0000 0000 0000 0000 0000 1111
hash2 1111 1111 1111 1111 0000 1111 0001 0101
&结果 0000 0000 0000 0000 0000 0000 0000 0101 = 5(index = 5的位置)
在数组长度为16的时候,他们两个hash值的位置是一样的,出现一个hash冲突的问题,用链表来处理
如果数组的长度扩容为32以后,重新对每个hash值进行寻址,也就是每个hash值跟新数组的length-1进行与操作
n-1 0000 0000 0000 0000 0000 0000 0001 1111
hash1 1111 1111 1111 1111 0000 1111 0000 0101
&结果 0000 0000 0000 0000 0000 0000 0000 0101 = 5(index = 5的位置)
n-1 0000 0000 0000 0000 0000 0000 0001 1111
hash2 1111 1111 1111 1111 0000 1111 0001 0101
&结果 0000 0000 0000 0000 0000 0000 0001 0101 = 21(index = 21的位置)
判断二进制结果中是否多出一个bit的1,如果没多,那么就是原来的index,如果多了出来,那么就是index+oldCap,oldCap就是原来的数组长度,通过这个方式,就避免了rehash的时候,用每个hash对新数组.length取模,取模性能不高,位运算的性能比较高
标签:hash冲突 源码 性能 一个 style 位运算 put hashmap 元素
原文地址:https://www.cnblogs.com/zhangliang1726/p/12170669.html