HashMap底层实现原理
HaspMap实现原理
我们先看一下从put方法入手来分析这个一下这个流程, 看下面流程图
我们看完这个图片后心中应该有个大概了 用与运算计算出数组下标—判断链表值是否相等如相等覆盖否则向下添加—如果链表大于8就转成黑数
到这里是不是觉得有点抽象,我在网上又找了几张图片可以让我们更加清楚的了解它的原理。
put(K, V)
put(K, V) 相同hash值
其实put和get和remove原理都是一样, 通过与运算计算下标然后判断是否是红黑树类型 如果不是: 根据单链表特性来获取和删除。如果是: 转换成红黑树判断进行查找和删除动作。
理解HashMap设计
1.如何计算链表数组下标
从上面源码中可以得到这套公式:(数组的大小 - 1) 与运算 hash值 = 下标。
那这套公式的作用是什么呢?
他可以帮助我们把数据尽量平均分配给每个下标,减少冲突减少查询时间。
这时候我们就想到为什么不用Key值的HashCode值与HashMap的长度进行取模运算,即 index = HashCode(Key) % hashMap.length这样也能平均分配。但是!这种取模方式运算固然简单,然而它的效率是很低的,浪费性能。所以jdk用了与运算的方式index = HashCode(Key) & (hashMap.length - 1)
2.如何降低冲突(加载因子为什么是0.75?)
从这里我们就可以看出来他判断扩容的依据是什么了, 容量 * 加载因子 = 判断resize的依据, 已知这套公式得出出下面这些结果。
- 日常初始化的时候尽量根据实际情况, 按照2的幂次方来设置初始值. 避免冲突降低多次扩容减少rehash次数
- 如果个加载因子过高虽然减少了rehash次数, 提高了空间利用率, 但是同样会导致冲突增大,增加查询实际成本。
- 如果个加载因子过低虽然减少了查询时间成本, 但是空间利用率很低,同时提高了rehash操作的次数。
- 为什么要选择0.75作为默认的加载因子,完全是时间和空间成本上寻求的一种折衷选择,
3.为什么初始容量要用2的次幂
举例:
“abc”十进制hashcode为96354,二进制为1 0111 1000 0110 0010,HashMap的默认长度为16,与15进的二进制1111位运算, 得到index = 2;
可以看出来,hash算法得到的index值完全取决与Key的HashCode的最后几位。这样做不但效果上等同于取模运算,而且大大提高了效率。
如果不是会怎样呢?
我们假设HaspMap的初始长度为10,要与9的二进制1001进行位运算。得到0,
但是 如果 hashCode为1 0111 1000 0110 1001 或者1 0111 1000 0110 1011这样会造成 有些索引出的值挤压太多。无法更好的分布均匀。
总结
- 往往我们系统申请内存的时候要2的倍数, 内存分页 往往4k
- 提高运算速度
- 避免内存碎片
- 增加散列度, 避免冲突
4.在什么情况下转成红黑树
在链表的长度超过了8,且下标的数量必须大于64如果小于64的时候只会扩容
总结
其实原理不是很难,底层的一些运算比较巧妙, 合理使用与运算和位运算有助于程序提高效率。