文章目录
基础
- HashMap中对相同对象的判定:当且仅当
hashCode一致
,并且equals一致
的对象,才会被HashMap认定为同一个对象。 Hash冲突
:
哈希是通过对数据进行再压缩,提高效率的一种解决方法。但由于通过哈希函数产生的哈希值是有限的,而数据可能比较多,导致经过哈希函数处理后仍然有不同的数据对应相同的哈希值。这时候就产生了哈希冲突。
如果多个对象被Hash函数
算出来的地址(桶索引)相同,那么这些对象就发生了Hash冲突,好的Hash函数会尽量避免发生Hash冲突。
- HashMap解决Hash冲突的方法:
链式地址法
,对于相同的Hash值,使用链表
进行连接,然后使用数组
储存链表。
Java8之前的HashMap
1.底层实现
数组+链表
2.成员变量和常量
- transient Entry<K,V>[]
table
;
table储存的是Entry数组,每个Entry是一个Hash桶,每个Entry都对应一条链表
,hash冲突的数据,都会被放到同一条链表中。 - transient int
size
键值对的实际数量 - final float
loadFactor
负载因子,决定table的扩容量 - int
threshold
确定是否需要扩容的阈值 - static final int DEFAULT_INITIAL_CAPACITY=16
初始化容量,默认为16,必须为2的幂 - static final int MAXIMUM_CAPACITY = 1<<30
最大容量 - static final float DEFAULT_LOAD_FACTOR=0.75f
默认负载因子
3.put(K,V)方法
public V put(K key, V value) {
//如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(24=16)
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key为null,存储位置为table[0]或table[0]的冲突链上
if (key == null)
return putForNullKey(value);
int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
int i = indexFor(hash, table.length);//获取在table中的实际位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
addEntry(hash, key, value, i);//新增一个entry
return null;
}
- 流程:使用
hash(key)
方法计算key的Hash值,使用indexFor(hash,table.length)
方法计算存入数组的索引,如果索引位置没有元素,就使用addEntry
方法添加键值对,如果有元素,就沿着链表查找,如果key是相同的,就更新值,不相同的话,使用addEntry
方法添加键值对。 - hash(Object key)
该方法计算key的hash值,hash算法应当尽可能的使哈希值分散,这样可以减少hash冲突,将元素尽可能的放在不同的hash桶中,即满足松散哈希
。 - indexFor(hash,table.length)
该方法根据hashcode和table长度计算该元素应当放在table数组的哪个索引位置
,hash值小于length-1时,返回hash,大于length-1时,取余数,由于取余数的计算成本较高,因此hashmap规定table的长度为2的幂
,这样可以通过h&(length-1)
即可取余数。 - addEntry(hash,key,value,index)
该方法在指定索引位置添加键值对,如果键值对将要占用的位置不是null,并且size>=threshold
,就会启动扩容方法resize(length),table的长度扩容为之前的2
倍。 - resize()
根据新的容量,确定新的threshold的值,简单来讲,新的threshold的值 = 新容量 * loadFactor,然后根据uesAltHashing判断是否需要进行Hash重构,最后使用transfer
方法重新计算所有键值对的新的数组索引。
问题:loadFactor对HashMap的影响?
负载因子的大小对HashMap的影响很大,太小了会导致HashMap频繁扩容,太大了会导致空间的浪费,因此需要在时间和空间上进行权衡,0.75是Java提供的建议值。
4.get(K)方法
- 流程:先找到key对应的索引,然后沿着链表找对应的键值对
- 可以看出get方法的性能关键在链表的长度上
5.性能分析
- 在进行put操作时,消耗资源的操作是
遍历链表
,扩容数组
- 在进行get操作时,消耗资源的操作时
遍历链表
因此针对遍历链表
操作,Java8中进行了优化。
Java8的HashMap
1.底层实现
数组+链表+红黑树
2.hash(key)方法
进行了简化,之前hash计算比较复杂是为了将hash值尽量分散,但我们注意到,HashMap的效率问题还是出在链表部分的遍历上,因此主要需要提高链表部分的遍历效率
。
3.putVal()方法
- 第一步,计算下标,与之前一致
- 第二步,当size超过threshold时,需要扩容
- 第三步,保存数据,分为3种情况
- 下标位置为null,直接添加一个链表节点
- 下标位置为树节点,添加或覆盖一个树节点
- 下标位置为链表节点,覆盖或添加一个链表节点,但在添加链表节点时,需要判断链表长度如果为7(建树阈值-1),那么需要调用
treeifyBin
方法,将链表转换为红黑树.
问题:为什么选择8作为建树阈值(TREEIFY_THRESHOLD)?
链表的查找是O(n),红黑树的查找是O(logn),在n太小的时候,它们的查找效率相差不大。
4.resize方法
//todo:还没太懂
- 第一步,和之前一样,重新规划table长度和阈值
- 第二步,重新排列数据结点:遍历table上的每个结点
- 如果结点为null,不进行处理
- 不为null,没有next结点(只有1个结点在这个索引),重新计算hash值,存入新的table
- 为树结点,调用该树结点的split方法处理,对红黑树进行调整,如果红黑树太小,就将其退化成链表
- 链表结点:对于hashcode超出旧容量和未超出旧容量的链表分别处理,对于hash&oldCap==0的部分,不需要处理,反之,利用公式:新索引 = 原索引+旧容量,放到新的下标位置上。