前言
近几日写代码时看到了老师使用缓存去处理一些需要经常从数据库中被查询的东西,而缓存用到了HashMap,因为考研时学到了Hash存储,所以借此再去了解一下HashMap.
Hash
Hash是将任意长度的数据映射出有限长度的数据,用来代表源数据,小到一串数据,大到任意类型任意大小的文件都可以通过一定的算法来映射出一串数据,并且相同的可能性极小,所以hash常用来进行文件完整性校验。
常见的hash算法如MD-5,SHA-1,SHA-256等。
这些一常用于密码加密。
我们拿java中的String.hashCode()函数为例,展示一个hash算法的计算过程
public int hashCode() {
int h = hash;
// default 0
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0;
i < value.length;
i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
我们可以看到,hashCode()函数循环乘加字符串中的每个字符,得到最终的hash值。
Hash存储 如果仅仅根据得到的hash值来存储则会大大浪费存储空间,如hash值为123456789则存到123456789号位置上,hash值为987654321则存到987654321位置上,那么会极大浪费空间。所以截下来我们用到hash函数进行再次映射。
学过数据结构的同学应该有所了解,hash存储有一个散列表,所有的数据都存放在散列表中的相应位置上。散列表长度固定,比如散列表长度为10,那么表中就有0,1,2...9十个位置。我们所要存放的数据得到hash值通过hash函数映射到这10个位置中。
文章图片
为了到达此效果,有很多构造散列值的算法,如除留余数法、平方取中法、折叠法、随机数法、数学分析法等。我们以最常见的除留余数法,就是把键值通过一个固定的算法函数既所谓的hash函数转换成一个整型数字,然后将该数字对散列表长度进行取余,取余结果就当作散列表的下标(散列值)。
文章图片
碰撞处理
不同的hash通过算法难免得到相同的存储位置,为了解决这种碰撞我们需要进行处理。
解决碰撞的方法有很多种,如线性探测法、拉链法等等。
线性探测法就是如果通过hash函数算出的位置上已经有数据了,那就看紧邻的下一个位置有没有数据,没有数据就放到这个位置上,依次类推。
文章图片
如31就是应该在6的位置上,但是6的位置上已经有27了,所以我们找7位置,7位置没有数据,我们放到7位置上。
负载因子=被除数/散列表长度
如负载因子为0.75,被除数为12,在散列表长度为16。
拉链法是每个存储位置存储一个指针,每个指针都指向一个链表。将通过hash函数算出的相同位置的数据依次放到链表上。
文章图片
HashMap hashMap也是散列表,同时也使用拉链处理冲突。
文章图片
HashMap的默认初始容量为2^4(=16),即使创建时指定初始容量,HashMap内部也会计算一个不小于指定容量的、最接近的且为2的整数次幂的一个值作为初始容量。
为什么是2的整数次幂?
因为我们在hash存储时使用了除留余数法,但是求余数计算比较复杂。所以我们简化为与2^n-1进行与操作。从而得到2的n次方个不同的结果。如果一个数据对应二进制为11011011,那么它与1111的与运算得到后四位1011。
如果不是2的整数倍,比如说容量为12,那么对应每个hash值与1011(11)进行与运算,那么10111101与1011与运算为1001,那么X1XX位置上永远不会有数据。
同时也对拉链表进行优化。我们想一种最坏情况,如果所有数据集中到同一链表中,那么我们查找起来就像在查找一个顺序表一样。
为了解决这个问题,我们在链表过长时,优化成为红黑数,一般当链表长度超过8时,使其变成红黑数,红黑数可以理解为二叉搜索树的一种。反之当长度小于6时,将红黑数变为链表。
文章图片
HashMap存储键值对 hashMap存储键值对通过Entry类,用Entry数组存储键值对,Entry类里面有四个属性:hash、K、V、next,分别存储哈希值、键对象、值对象、下一个Entry对象引用。
文章图片
Entry源码
文章图片
Entry对象结构图
文章图片
Entry存储结构图。
通过对key对象进行hashCode(),获取hash值,然后与运算获取存储位置。
ConcurrentHashMap ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些。
应用 【Hash与HashMap】这种存储方式我们每天都会使用,那就是mysql的索引。