HashMap实现类
HashMap默认的初始容积大小为16,加载因子默认0.75,threshold阈值为【容积*加载因子】
HashMap采用的是链表法解决哈希冲突问题,同时引入红黑树可以避免单个链表长度过长的问题
-
默认8将单向链表转换为红黑树,注意这里还有一个条件默认64,只有集合中的结点数大于64时才可能进行树化处理
-
默认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的幂数
-
如果没有特殊要求不要更改参数,因为JDK自带的默认负载因子是适用于通用场景需求的
-
如果确实需要修改,建议不要超过0.75,因为过大的负载因子值会显著增加冲突,降低hashma性能
-
如果使用太小的负载因子,可能会导致频繁的扩容,增加性能开销,本身访问性能会受到影响
putVal中有2次resize操作,分别是第一次初始化时扩容或者数组的实际大小大于临界值。扩容时会伴随桶上元素的重新分发。jdk1.8是根据同一个桶的位置中进行判断(e.hash & oldCap)是否为0,如果不为0则移动带新位置【原始位置+增加的数组大小】
put总结
当put新元素时,首先计算key的hash值,这里会调用一个hash方法,hash方法时key.hashCode()与key.hashCode()>>>16进行高低位的异或计算,所以hash函数的作用是:高16位不变,低16位和高16位进行异或计算,从而尽量减少hash碰撞的概率
因为数组中元素bucket的个数是2的幂,计算元素存储的数组下标方法index=(table.length-1) & hash,如果不进行hash处理,则散列生效的值只有低几位的bit值,为了减少hash碰撞,所以使用高低位异或计算以减少碰撞。而且使用时间复杂度为O(logN)的红黑树结构提升碰撞下的性能
一般建议使用String或者Integer这样的包装类作为key
String、Integer之类的包装类特性能够保证hash值的不可更改型和计算准确性,能有有效地减少hash碰撞的概率
1、都是final类型并且具有不可变性,保证key值的不可更改,不会存在获取key值不同的问题
2、内部已经重写了equals和hashCode方法,遵守了HashMap内部规范,不容易出现hash计算错误问题
自定义对象作为key
必须重写hashCode和equals方法
-
重写hashCode方法是因为需要计算存储数据的位置
-
重写equals方法,需要遵守自反性、对称性、传递性、一致性
hashmap如何解决哈希冲突
1、使用链地址法来链接拥有相同hash值的数据
2、使用2次扰动来降低hash冲突的概率,使数据分布均匀
3、引入红黑树进一步降低遍历的时间复杂度