- 数据结构
由数组+链表组成,主干是一个Entry数组,每一个Entry包含一个Key-Value键值对,链表存在的主要意义在于解决哈希冲突,从JDK1.8开始加入红黑树,主要目的在于解决链表元素过多的问题。
- put的过程
public V put(K key, V value) {
//如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(24=16)
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key为null,存储位置为table[0]或table[0]的冲突链上
if (key == null)
return putForNullKey(value);
int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
int i = indexFor(hash, table.length);//获取在table中的实际位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
addEntry(hash, key, value, i);//新增一个entry
return null;
}
看以上这段代码我们需要重点关注到这几个点:
1、inflateTable方法用于为主干数组table在内存中分配存储空间,数组长度一定为2的次幂(至于为什么后面再说),默认为16。
2、hash函数是一个神奇的函数,对key的hashcode进一步进行计算以及二进制位的调整等来保证最终获取的存储位置尽量分布均匀。
3、indexFor方法用于进一步处理来获取实际的存储位置,具体执行是利用hash函数计算出的值与数组长度length-1做与操作。
最终的实现流程如下图所示:
- 为何数组长度一定是2的次幂?
如果数组进行扩容,数组长度发生变化,那么存储位置也可能发生变化,重新计算每个元素的存储位置将是很大的一笔开销。
如果数组长度限制为一定是2的次幂会发生什么?
比如16的二进制表示为10000,那么length-1就是15,二进制为01111,同理扩容后的数组长度为32,二进制表示为100000,length-1为31,二进制表示为011111。
很明显我们可以观察到,这样能够保证低位全为1,而扩容后只有一位差异,也就是多出了最左位的1,这时只要h对应的最左边的那一个差异位为0,就能保证得到的新的数组索引和老数组索引一致,这将大大降低老数组元素存储位置需要重新调整的概率,这是原因之一。
还有个重要的意义在于低位全为1时,会使得获得的数组索引index更加均匀散列,大大避免哈希冲突(哈希碰撞)现象的产生,结论是这样,那么何解呢?自己随便写一个h值和低位全为1的值做与运算看看,是不是可以很清楚地发现h低位部分任何一位的变化都会对最终结果产生影响,h低位部分具备唯一性,而如果h值和低位不全为1的值做与运算,h低位部分将不再具备唯一性,哈希冲突的概率大大增加,同时一旦存在不为1的bit位,那么注定会造成某些数组位置白白浪费的情况(个人理解,不好体会的需要细品)。