HashMap数据结构
HashMap是一个数组队列,默认长度为16。
new HashMap<?K,?T>:new ArrayList<?K,?T>(16);
HashMap的底层特点
以空间换时间。
如何理解呢? 我的理解是按照某属性二次hash进行分类,我们的数组队列本来是:
[{key:1,value:1},{key:1,value:2},{key:2,value:2},{key:2,value:3}]
二次hash后=>
[{key:1,value:[1,2]}{key:2,value:[2,3]}]
那么我们的查询就可以分类查询,只查询 key=1或者key=2的,当然这只是个举例。
HashMap的二次hash要比这个复杂一些:
当我们数组长度很大时,查询一个元素甚至可能要遍历整个数组,
进行二次hash的意义就在于分区,
HashMap的二次hash结果如下:
其中
a的散列值(hashCode): 97 b的散列值:98
...
...
r的散列值:114
因为b的hashCode和r的hashCode进行hash算法得到的下标都为2,
相当于,我们把元素key值为"b""和"r"的2个元素分为一区,当我下次查询r时就不需要遍历到最后,因为我知道r元素是在下标为2的分区中,因此,节省了查询时间。
[
[{a:1}]
[{b:2},{r:3}]
]
在下面具体介绍hash方法。
HashMap二次hash
HashMap是按照新增元素的key值的hashCode进行二次hash的。
hashCode就不赘述了。
//jdk1.7
hash:(int h)->{
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
indexFor:(h, length) {
return h & (length-1);
}
indexFor(hash(key.hashCode()),hashMap.size()) // 取模获取下标
//jdk1.8
hash:(key)->{
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
putVal:(hash)->{
...
tab[i = (n - 1) & hash] // (n - 1) & hash 取模获取下标
...
}
上面是1.7 和 1.8 的二次hash的实现,先不管差异,先看1.7
插入元素获取下标步骤:
在新增元素时,取新增元素的key的hashCode和(hashMap的长度-1)做 按位与运算,得到我们新增元素的下标
如"b":下标为2,将key为b的元素插到下标为2的位置(也就是Entry1)
这样下次查询(HashMap.get(key))的时候,我们就可以直接通过key的hash算法找到存放的下标然后直接获取了。
我们继续向hashMap中插入元素(重复插入元素获取下标步骤),
...
...
直到我们准备插入key为"r"的元素时,发现"r"的下标也是2,怎么办呢?
HashMap使用链表来解决下标重复的问题,
将所有数据,按照key的hashCode进行二次hash,从而转换为多个链表。
这是HashMap实现底层特点的原理。以空间换时间。
这里的Entry6就是新增的"r"元素了,Entry1自然就是之前的"b"元素。
HashMap的默认长度为16,当数据量越接近HashMap的长度时,相对来说,就更容易出现下标一样的情况,
HashMap有一个属性叫做负载因子“DEFAULT_LOAD_FACTOR”,默认值为0.75,作为阙值
当数据量>= HashMap长度 * 0.75的时候,HashMap会进行扩容,
从16 变为 32,或者 32 变为 64, ...
因为hash的时候会进行位运算,长度变大了意味着位运算的位数变大了,运算出来的结果变多了,也就意味着,分区变多了。
如
长度为16 进行位运算,
2 & (16-1) - > 00001111 & 0000010 => 最多出现 16种情况,刚好分布到16个区(长度16)
长度为32 进行位运算,
2 & (32-1) - > 00011111 & 0000010 => 最多出现 32种情况,分布到32个区
Q&A:
为什么要把key进行hash呢?
假如有100条数据,我们通过某种共同点将他们分成5部分,那么查询的时候,只用遍历五分之一,这就是hash的好处。 100 -> 5,将100条数据平均分布到5就是hash的功能。
为什么默认长度是16?
其实默认长度只要是2的幂数都可以。 只不过,2,4,8太小了,会频繁扩容,也容易发生hash碰撞,默认32又太大了有点浪费,所以选择16这个中位数。
为什么 h & (length-1)?
为了满足按位与运算,我们想要"1111"全部为1的数去完成平均分布的任务。
(0和1 遇到 1 进行与运算,得到0和1的结果的概率稳定为0.5)。
jdk1.7 和 1.8 hash实现区别
为什么要扩容,链表也可以查询
链表是最坏的解决办法,也就是说长度为16的HashMap,插入12条数据,12条数据都在同一个分区,那么链表是可以满足的。 但是如果不扩容, 10000条数据,也分布在长度16的HashMap中,甚至也可能在同一个区,也就是有一条 长度10000的链表,那么查询起来一样痛苦,所有需要扩容,扩容的意义等同于提高分类精度。
为什么负载因子默认值为0.75?
如果是0.5 , 那么每次达到容量的一半就进行扩容,默认容量是16, 达到8就扩容成32,达到16就扩容成64, 最终使用空间和未使用空间的差值会逐渐增加,空间利用率低下。 如果是1,那意味着每次空间使用完毕才扩容,在一定程度上会增加put时候的时间。
为什么是0.75,不是0.6或者0.8?
HashMap新增数据
HashMap新增时,会通过hash方法确定将要新增的元素的下标。
put:(key,value)->{hashMap.get[hash(key,value)]=value}
小结
未完待续.. 个人理解,有错请指出,谢谢!