目录
?
HashMap基于Hash算法实现的
?简单总结
为什么HashMap中String、Integer这样的包装类适合作为Key
要让自己的Object作为Key应该怎么办呢
HashMap是怎么解决哈希冲突的
?JDK1.8新增红黑树
总结:
HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标
?为什么数组长度要保证为2的幂次方呢
?为什么是两次扰动
?
HashMap基于Hash算法实现的
当往Hashmap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标 hashmap存储时,如果出现hash值相同的key,此时有两种情况。 如果key相同,则覆盖原始值 如果key不同出现冲突,则将当前的key-value 放入链表中[尾插法] 按照key值获取数据时,直接找到hash值对应的下标,在进一步判断key是否相同从而找到对应值
HashMap解决hash冲突的问题核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。 需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过8个后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)?
?简单总结
当put的时候,首先计算key的hash值,这里调用了hash方法,hash方法实际是让key.hashCode() 与key.hashCode()>>>16进行异或操作,高16bit补0,一个数和0异或不变,所以hash函数大概的作用是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。
按照函数注释,因为bucket数组大小是2的幂,计算下标index = (table.length-1)&hash,如果 不做hash处理,相当于散列生效的只有几个低bit位,为了减少散列的碰撞,设计综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8+中用了复杂度O(logn)红黑树结构来提升碰撞下的性能。
为什么HashMap中String、Integer这样的包装类适合作为Key
String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率
1. 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况 2. 内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范,不容易出现Hash值计算错误的情况;
要让自己的Object作为Key应该怎么办呢
重写hashCode()和equals()方法
重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞 重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性
HashMap是怎么解决哈希冲突的
在Java中,保存数据有两种比较简单的数据结构:数组和链表。
数组的特点是:寻址容易,插入和删除困难;
链表的特点是:寻址困难,但插入和删除容易;所以将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做链地址法的方式可以解决哈希冲突
这样就可以将拥有相同哈希值的对象组织成一个链表放在hash值所对应的bucket下,但相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或)
}
?这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动)
?JDK1.8新增红黑树
通过上面的链地址法(使用散列表)和扰动函数我们成功让我们的数据分布更平均,哈希碰撞减少,但是当我们的HashMap中存在大量数据时,加入我们某个bucket下对应的链表有n个元素,那么遍历时间复杂度就为O(n),为了针对这个问题,JDK1.8在HashMap中新增了红黑树的数据结构,进一步使得遍历复杂度降低至O(logn)
总结:
?简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:
1. 使用链地址法(使用散列表)来链接拥有相同hash值的数据; 2. 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均; 3. 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快
HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标
hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~`2 ^ 30`,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置
HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行 进行异或运算,降低哈希碰撞概率也使得数据分布更平均在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;
?为什么数组长度要保证为2的幂次方呢
只有当数组长度为2的幂次方时,`h&(length-1)`才等价于`h%length`,即实现了key的定位2的幂次方也可以减少冲突次数,提高HashMap的查询效率; 如果length为2的次幂则length-1转化为二进制必定是11111……的形式,在于h的二进制与操作效率会非常的快,而且空间不浪费;如果length不是2的次幂,比如length为15,则length-1为 14,对应的二进制为1110,在于h与操作,最后一位都为0,而0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费
?为什么是两次扰动
两次扰动就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的
|