核心参数
hashmap其内部有
- threhold:实际的负载量,超过了这个值,就会扩容。
- loadFactor:扩容因子。
- 真实容量(capacity) * loadFactor = threhold
- Entry<K,V>[] 这是个Entry<K,V>的数组,代表了整个hash表,其中每一个Entry<K,V>代表一个桶,他是一个链表。正因如此,即使发生了哈希冲突,也可以放在同一个桶中。
- modCount
我们知道HashMap不是线程安全的 , 也就是说你在操作的同时可能会有其它的线程也在操作该map, 用modCount来记录修改集合修改次数。
我们在边迭代边删除集合元素时会碰到一个异常ConcurrentModificationException , 原因是不管你使用entrySet()方法也好 , keySet()方法也好 , 其实在for循环的时候还是会使用该集合内置的Iterator迭代器中的nextEntry()方法 , 如果你没有使用Iterator内置的remove()方法 , 那么迭代器内部的记录更改次数的值便不会被同步 , 当你下一次循环时调用nextEntry()方法便会抛出异常。这和我们学习ArrayList里面的原理是一样的。ArrayList学习笔记
构造
- 首先我们new一个hashMap,点进去
如下:
查看两个默认值看到一个为16,一个为0.75 ,这也就是一会要赋给的threhold和loadFactor的默认值
- 在下面这个构造函数我们可以看到,把传进来的两个默认值分别赋给了threhold和loadFactor
- new hashMap无参构造会把默认的DEFAULT_INITIAL_CAPACITY 16 赋给 threshold。会把DEFAULT_LOAD_FACTOR 0.75 赋给loadFactor。
- 会在首次put时把threshold设置为 16 * 0.75.
put方法详解
-
首先看到put方法主体
-
如果map是空的话 会初始化 通过
inflateTable(threshold);
将原本容量转化成二进制 (向上取整)赋值给了capacity,作为实际容量,如 7转化成8 9转化成16具体原因下面会讲。
然后将threshold 变成了 新的容量 capacity 的loadFactor(扩容因子)倍 。 -
第一次put时会初始化数组,其容量变为不小于指定容量的2的幂数。然后根据负载因子确定阈值。
-
如果键值为空,会进入如下方法:
这个方法所代表的含义就是,查看整个hash表的第一个桶(是个链表),看这个链表中有没有键值为空的,如果有,就将val赋值给它。如果没有,就进入addEntry
方法。方法如下:
这个方法所代表的含义就是:如果负载即threhold没有满,或者size超过负载量了但指定的桶中为空,那么进入createEntry
方法:在当前桶中头插入这个节点。
如果size超过负载了,并且指定的桶有元素了就会进入if里面:先扩容2倍,在重新计算hash值,然后根据新的hash值获得桶的索引,再次插入。 -
如果键值不是空的,就会根据键值计算出hash,然后根据hash计算出索引。
计算索引方法如下:
这也是为什么必须要求hash表的长度是2的幂次方。再减一之后,除了最高位,其余位都是1,在和hash相与,得出结果。 (这个过程就是求余) 我们可以看到如果容量是2的次方 , 那么length - 1得到的二进制的除了补位外都是1,根据&运算符的规则 , 0&0=0; 0&1=0; 1&1=1;那么也就意味着不论h的值是什么 , 只要length - 1的二进制码是这样的规律的 , 那么就 可以保证hash的值只有和length - 1的同位参与了运算 , 例如二进制码A(10101011)&B(00001111)的结果就是
C(00001011) , C的结果只会受到b二进制码后四位的影响,因为b的补位都是0 , 也就是说h & (length - 1)得到的索引不会大于length,也就不会越界。
计算出索引之后就进入了for循环。for循环 遍历的是上面计算出来的桶索引代表的那个链表的所有节点。然后再判断这个链表里面有没有 和要插入的节点hash值相同并且equals或者==结果也相同的节点,如共有,就覆盖,并且返回旧值。否则就插入节点。
get方法详解
- get方法相比来说就比较简单了
我们点进get方法首先看到:
如果键值时空的话,会走如下方法
这个和刚才看的put方法一致,都在再桶索引为0的地方存取,不用多说。
当键值不为空的时候进入下面方法
这个方法的含义是:首先根据键值计算出hash,然后根据hash计算出索引找到桶,遍历链表的每个节点,当遇到和目标hash相同并且key相同或者equals的节点,就返回。
resize方法详解
- 这是一个扩容方法,由图可见:新容量= 旧容量 * 2
新负载= 新容量*扩容因子
其中最主要的就是transfer方法,他把旧table的值都给了newtable
用头插法插入新的hashTable因为是头插法,所以会导致链表反转
元素迁移的过程中在多线程情境下有可能会触发死循环(无限进行链表反转)。
出现环形链表的原因大概是这样的:线程1准备处理节点,线程二把HashMap扩容成功,链表已经逆向排序,那么线程1在处理节点时就可能出现环形链表。
transfer方法详解
参考链接:1. https://zhuanlan.zhihu.com/p/114363420
2. https://blog.csdn.net/F1004145107/article/details/105540106?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.pc_relevant_default&utm_relevant_index=2