HashMap实现类
HashMap默认的初始容积大小为16,加载因子默认0.75,threshold阈值为【容积*加载因子】HashMap采用的是链表法解决哈希冲突问题,同时引入红黑树可以避免单个链表长度过长的问题。
1.默认8将单向链表转换为红黑树,注意这里还有一个条件默认64,只有集合中的结点数
大于64时才可能进行树化处理。
2. 默认6将红黑树退化成链表。
hash函数的涉及需要考虑简单高效和分布均匀两个方面,所以首先获取key对象的hashCode值,然后要将hash值的高位和低位进行与运算后,再针对数组长度进行求余。
HashMap线程不安全,进行多线程操作时可能会出现扩容时执行rehash操作的死循环问题、脏读导致数据丢失问题和size值不精确的问题。
put方法实现流程
public V put(K key, V value) { // 向hashmap集合中添加一个key/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;
// 1、如果table为空或者长度为0,那么调用resize方法扩容数组.实际上resize方法兼容了两个职责,创建初始化数组或者容量不足时进行扩容处理
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2、计算插入数据存储到数组的对应索引值,如果数组为空则不存在hash冲突,则直接插入。这里的hash值时通过hash(key)获取的
if ((p = tab[i = (n - 1) & hash]) == null)// key值对应的hash值获取在哈比表中存储的索引下标 hash%n
tab[i] = newNode(hash, key, value, null);
else {// 如果桶上已经存储了数据
Node<K, V> e;
K k;
// 2.1、判断table[i]的元素是否与需要插入的key值一样,如果相同则世界使用新的value覆盖旧有的value
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 判断原则时调用key对象中的equals方法
e = p;
// 2.2、继续判断需要插入的数据结构是否为红黑树还是链表,如果红黑树则直接在树中注解插入或者更新键值对
else if (p instanceof TreeNode)
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
else {
// 2.3、遍历table[i],判断key是否已经存在,采用equals对比当前遍历节点的key与需要插入的数据的key,如果相同则直接覆盖
// 2.4、遍历完毕后发现没有出现对应的key,则直接在链表尾部插入数据,插入完成后判断链表的长度是否大于8,如果是则进行树化处理,将链表转换为红黑树
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;// fail-fast检测
if (++size > threshold) // 判断当前集合中的元素个数是否大于阈值,如果大于阈值则进行扩容处理
resize();
afterNodeInsertion(evict);
return null;
}
hash方法putVal(hash(key), key, value, false, true)
static final int hash(Object key) {
int h;
//当key为null,则直接返回hash值为0
//当key不为null时,首先调用key中的hashCode方法获取key的hash值,然后将hashCode值向左移动16位,然后进行异或计算。【将高位的hashCode值和低位的hashCode值进行异或计算】
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
将key的hashCode值的高16位和低16位进行异或计算,这是因为有些数据计算出的哈希值差异在于高位,但是HashMap里的哈希寻址【求余操作】时忽略容量以上的高位值,高低位异或扰动计算的目的是避免类似情况下的哈希碰撞。
resize()方法的说明:将table大小初始化或加倍。如果为null,则按照默认值初始数组(16、0.75)。否则,使用2倍扩容处理,因为每个容器中的元素必须保持在相同的索引中,或者移动在新表中具有二次方偏移。
涉及的参数
hashMap容量、负载因子和树化操作
预先设置的数组长度需要满足大于【预先估算的元素数量/负载因子】,同时还必须是2的幂数。
1.如果没有特殊要求不要更改参数,因为JDK自带的默认负载因子是适用于通用场景需求的。
2.如果确实需要修改,建议不要超过0.75,因为过大的负载因子值会显著增加冲突,降低
hashmap性能。
3.如果使用太小的负载因子,可能会导致频繁的扩容,增加性能开销,本身访问性能会受
到影响。
putVal中有2次resize操作,分别是第一次初始化时扩容或者数组的实际大小大于临界值。扩容时会伴随桶上元素的重新分发。jdk1.8是根据同一个桶的位置中进行判断(e.hash & oldCap)是否为0,如果不为0则移动带新位置【原始位置+增加的数组大小】