1. HashMap的几种构造方法
1.1 空参构造
解释:当我们进行实例化HashMap类,不进行参数传递的时候,HashMap底层就只是进行了加载因子的初始化
加载因子:就是对当前HashMap是否需要扩容起决定性因素的一个因子,例如:现在HashMap的初始化长度为16,加载因子为0.75,这个时候扩容门槛(threshold)就是16*0.75 = 12,意思就是当HashMap中的节点(node)数量超过了12(不等于),将会触发扩容事件进行HashMap扩容。
1.2 带初始化HashMap长度的构造方法
解释:当你进行实例化HashMap的时候,如果传递了一个初始化长度的参数,HashMap底层就是调用了下面我需要叙述的另外一个构造方法
1.3 带初始化HashMap长度和加载因子的构造方法
解释:
- 首先,判断当前传递的初始化长度时候小于0,小于0,则抛出非法参数异常
- 第二,判断当前传递的容量是否大于HashMap的最大存储容量MAXIUM_CAPCITY,如果大于,则将初始化长度设置为最大长度
- 第三,判断当前的加载因子是否小于等于0或加载因子是否为NaN,如果是,则抛出非法参数异常
- 最后,传递参数都是合法的,将传递参数设置为HashMap的初始化配置值,其中涉及到的tableSizeFor方法,在这里暂时不赘述,后面我们进行叙述的时候会进行详细说明,现在你只要知道这个方法是针对你传递的容量参数进行优化,已达到HashMap的性能最佳即可
说明:MAXIUM_CAPCITY:默认值为1 >> 30,即2的30次方
1.4 带Map对象的构造方法
解释:如果初始化时传递了一个Map类型的对象,那么HashMap底层首先初始化加载因子为默认加载因子,然后调用putMapEntries进行批量加入元素,对于putMapEntries方法后面会有详细的说明
2. HashMap中的put方法
2.1 首先,跟进HashMap中的put方法,发现他调用了另外一个方法,叫做putVal
2.2 跟进putVal,重点来了!!!这里就实现了HashMap中的put的具体实现
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
2.2.1 逐行解读源代码
2.2.1.1 putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// 初始化一个用来存放当前HashMap的临时变量
// 为了避免后续操作多次从内存中取map而提高性能损耗
Node<K,V>[] tab;
// 也是一个中转变量,用来存放当前下标占用元素的地址值,后续会进行更详细的说明
Node<K,V> p;
// 初始化两个存放当前HashMap的长度和当前下标的变量
int n, i;
// 这里就是我上面所叙述的临时存放当前HashMap
tab = table
// 将当前HashMap的长度赋值给变量n,便于后续的使用
n = tab.length
// 判断当前HashMap是否为空或者HashMap的长度是否为空
if (tab == null || n == 0) {
// 扩容操作
tab = resize()
n = tab.length;
}
// 计算当前元素应该存入的位置
i = (n - 1) & hash
p = tab[i]
// 如果存入位置没有元素,则直接赋值即可
if (p == null)
tab[i] = newNode(hash, key, value, null);
else {
// 如果有元素
Node<K,V> e;
K k;
// 判断当前存储的元素的hash值是否相等
// 且key的引用是否相等或key值相等
// 注意:b2和b3的判断是为了避免哈希碰撞的情况发生,
// 例如:abc和acD的计算哈希值是一样的,这就出现了哈希碰撞
// 本来abc这个key和acD这个key是不冲突,但是通过计算得出冲突了,我们就可以通过二次比较key值是否相等的方式进行解决
k = p.key
Boolean b1 = p.hash == hash
Boolean b2 = k == key
Boolean b3 = key != null && key.equals(k)
if (b1 && (b2 || b3)) {
// 如果出现了hash值一样且key值一样,保存当前索引值
e = p;
}
// 如果没出现,判断当前元素是否是一个红黑树
else if (p instanceof TreeNode)
// 如果是则进行红黑树操作
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 如果没有出现上面的情况,则按链表处理
// 循环遍历链表
for (int binCount = 0; ; ++binCount) {
// 将下一个值赋值给临时变量
e = p.next
// 如果下一个值为空,则直接接在这后面
if (e == null) {
p.next = newNode(hash, key, value, null);
// 判断当前链表有没有达到红黑树化的门槛
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 如果达到了,就进行红黑树化,然后瑞出循环
treeifyBin(tab, hash);
break;
}
// ① 与上述同理
k = p.key
Boolean b4 = e.hash == hash
Boolean b5 = k == key
Boolean b6 = key != null && key.equals(k)
if (b3 && (b4 || b5){
break;
}
p = e;
}
}
// 这一个if判断其实可以接到①所在代码块中,其实就是在当发生了key值一样的时候,需不需要进行覆盖操作
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 这里的onlyIfAbsent,就是我们在putVal中传递的参数
// 当发生了key相等 且 onlyIfAbsent为false或当前存储的值为空时,进行覆盖操作
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 然后返回覆盖前的值
return oldValue;
}
}
// 判断当前容量是否达到了扩容阈值
if (++size > threshold)
resize();
// 没有值被覆盖,则返回null
return null;
}
2.2.1.2 resize扩容方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// 初始化一个用来存放当前HashMap的临时变量
// 为了避免后续操作多次从内存中取map而提高性能损耗
Node<K,V>[] tab;
// 也是一个中转变量,用来存放当前下标占用元素的地址值,后续会进行更详细的说明
Node<K,V> p;
// 初始化两个存放当前HashMap的长度和当前下标的变量
int n, i;
// 这里就是我上面所叙述的临时存放当前HashMap
tab = table
// 将当前HashMap的长度赋值给变量n,便于后续的使用
n = tab.length
// 判断当前HashMap是否为空或者HashMap的长度是否为空
if (tab == null || n == 0) {
// 扩容操作
tab = resize()
n = tab.length;
}
// 计算当前元素应该存入的位置
i = (n - 1) & hash
p = tab[i]
// 如果存入位置没有元素,则直接赋值即可
if (p == null)
tab[i] = newNode(hash, key, value, null);
else {
// 如果有元素
Node<K,V> e;
K k;
// 判断当前存储的元素的hash值是否相等
// 且key的引用是否相等或key值相等
// 注意:b2和b3的判断是为了避免哈希碰撞的情况发生,
// 例如:abc和acD的计算哈希值是一样的,这就出现了哈希碰撞
// 本来abc这个key和acD这个key是不冲突,但是通过计算得出冲突了,我们就可以通过二次比较key值是否相等的方式进行解决
k = p.key
Boolean b1 = p.hash == hash
Boolean b2 = k == key
Boolean b3 = key != null && key.equals(k)
if (b1 && (b2 || b3)) {
// 如果出现了hash值一样且key值一样,保存当前索引值
e = p;
}
// 如果没出现,判断当前元素是否是一个红黑树
else if (p instanceof TreeNode)
// 如果是则进行红黑树操作
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 如果没有出现上面的情况,则按链表处理
// 循环遍历链表
for (int binCount = 0; ; ++binCount) {
// 将下一个值赋值给临时变量
e = p.next
// 如果下一个值为空,则直接接在这后面
if (e == null) {
p.next = newNode(hash, key, value, null);
// 判断当前链表有没有达到红黑树化的门槛
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 如果达到了,就进行红黑树化,然后瑞出循环
treeifyBin(tab, hash);
break;
}
// ① 与上述同理
k = p.key
Boolean b4 = e.hash == hash
Boolean b5 = k == key
Boolean b6 = key != null && key.equals(k)
if (b3 && (b4 || b5){
break;
}
p = e;
}
}
// 这一个if判断其实可以接到①所在代码块中,其实就是在当发生了key值一样的时候,需不需要进行覆盖操作
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 这里的onlyIfAbsent,就是我们在putVal中传递的参数
// 当发生了key相等 且 onlyIfAbsent为false或当前存储的值为空时,进行覆盖操作
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 然后返回覆盖前的值
return oldValue;
}
}
// 判断当前容量是否达到了扩容阈值
if (++size > threshold)
resize();
// 没有值被覆盖,则返回null
return null;
}
HashMap的源码与本文有差异,为了方便大家阅读,进行了一些修改和删除,如有需要,请自行寻找相关完整源码阅读