为什么会出现哈希冲突
哈希冲突是指不同内容的对象调佣hashCode方法获取的哈希值相同,常见的原因之一就是,哈希值是一个int值,当计算过程中数值超过了int的最大值就会发生数据溢出,导致不同内容的对象,可能产生相同的哈希值。另外,就HashMap而言,由于为了获取桶索引的而进行的取模运算,导致即使不同的哈希值也可能得到相同的桶索引,也算是一种哈希冲突。
为什么HashMap的数组长度为2的n次幂、为什么(n - 1) & hash总是能落在数组范围内
首先思考问题,如何经过使用int值hash经过计算得到一个数组范围内的数字,答案是取模运算即hash % n , 又经过公式转换 当数组长度n为2的n次幂(两处的n并无关系)时 (n - 1) & hash 的结果与 hash % n 完全相等,但在计算机中 取模运算% 的效率不如 按位与 &,所以就用了(n - 1) & hash,且(n - 1) & hash的结果总是在数组范围内,并且这也解释了为什么HashMap的数组长度为2的n次幂。
hashCode和equals的关系
根据以上分析可以得出结论,哈希值相同的对象,equals不一定相同,但equals相同的对象则哈希值一定相同。
put方法源码解读
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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)
// 当map还没有初始化时,初始化数组table容量为16,设置负载因子loadFactor为0.75 设置阈值threshold为12(16 * 0.75)
n = (tab = resize()).length;
// 根据当前key的哈希值和table数组容量,计算出数组索引
if ((p = tab[i = (n - 1) & hash]) == null)
// 若该索引位置未被占用,就说明当前key不重复(因为相同的key的哈希值一定相同,相同的哈希值计算出的索引一定相同),则直接新增node节点加入到数组中
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 若该索引位置已被占用,则用当前节点比较哈希值和key,若都相同,则认为key重复
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// key重复,则将当前索引位置节点p赋值给节点e
e = p;
// 若当前节点key不相同,判断是否为树节点
else if (p instanceof TreeNode)
// 若该节点为树节点,则调用putTreeVal方法,该方法查找是否存在当前key的节点,若存在则返回,若不存在则新增节点插入到树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 循环遍历链表节点,结束条件是:没找到重复key,新增节点、找到重复key,修改节点
for (int binCount = 0; ; ++binCount) {
// 将下一个节点赋值给e ,并判断是否null
if ((e = p.next) == null) {
// 为null则说明这是最后一个节点,则说明链表内不存在重复key,则创建新的节点并插入链表
p.next = newNode(hash, key, value, null);
// 判断当前链表内节点数量是否大于等于8(因为从0开始遍历,所以需要+1)
if (binCount >= TREEIFY_THRESHOLD - 1)
// 若数量大于等于8,则将链表转为红黑树
treeifyBin(tab, hash);
// 新增数据成功,循环结束
break;
}
// 判断当前节点key与当前key是否相同
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相同则结束循环,当前节点已经赋值给e,后续代码会更改新的value
break;
// 若当前节点不是最后一个,key也不相同,则将当前节点赋值给p,以便继续循环能拿到下一个节点
p = e;
}
}
// 以上代码,在找到重复key时,会给当前节点赋值给e
if (e != null) {
// 保存当前节点value为旧的value以便后续返回
V oldValue = e.value;
// 因为put方法调用putVal方法时onlyIfAbsent=false,所以一定能进来
if (!onlyIfAbsent || oldValue == null)
// 更改当前节点value为新的value
e.value = value;
// 空方法
afterNodeAccess(e);
// 返回旧的value
return oldValue;
}
}
// 修改次数+1(根据观察,新增元素才会+1,修改不会)
++modCount;
// map元素数量+1,并判断是否超过阈值
if (++size > threshold)
// 若map元素size超过了阈值threshold则扩容数组table为原来容量二倍,阈值变为原来二倍,并重新计算索引、分配数据
resize();
// 空方法
afterNodeInsertion(evict);
// 新增数据返回null
return null;
}
putVal方法流程总结
默认容量16,阈值12,负载因子0.75
1.根据key的哈希值和容量计算得到数组索引。
2.若索引位置未被占用,就说明当前key不重复,新增节点加入数组。
3.若索引位置已被占用,则比较头结点,若相同则准备重写value。
4.若头结点不相同,则遍历链表或红黑树,找到相同key节点则准备重写value,若无相同key节点则新增链表节点或树节点,并加入链表或树
4.1新增链表节点时,判断当前链表内节点数量是否大于等于8,大于等于8则将链表转为红黑树。
5.若有相同key节点则将节点value更新,并返回旧的value,方法结束。
6.若新增节点,则修改次数modCount+1,并判断size是否大于阈值threshold,大于边扩容,扩容为当前容量二倍,阈值也是二倍,返回null方法结束。