1.什么是哈希表
进行增删改查操作时,使用哈希函数进行定位,在不考虑哈希冲突的情况下,其操作的时间复杂度都为O(1)
(哈希函数:存储位置=f(关键字))
2.哈希冲突(碰撞)
定义:使用哈希函数进行定位时,不同的元素映射到了相同的地址
解决办法:a.开放定址法(发生冲突,继续寻址) b.再散列函数法 c.链地址法(数组+链表)
HashMap使用的就是数组+链表的结构transient Node<K,V>[] table
3.HashMap核心字段
size:实际存储的key-value对的个数
capacity:容量(数组的长度)
loadFactor:负载因子
threshold:阈值(容量*负载因子),size达到阈值之后就要对Map进行扩容
4.put方法
jdk1.7版本的
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;
}
1.判断table是否为空数组,若为空,则依据threshold的值对table进行初始化
2.判断key是否为null,若为null,则存在table[0]或table[0]的冲突链上
3.针对key值,调用一系列函数,找到该元素要put在table数组中的实际位置i,定位到table[i]这条冲突链
4.遍历table[i]这条冲突链,判断要进行put的元素是否已存在,若已存在,则使用新value替换旧value并返回旧value
5.新增一个entry
jdk1.8版本的
HashMap使用的是链表来解决哈希冲突的,在数组上进行定位时使用的哈希函数,时间复杂度是O(1),但是链表上的操作还是使用的是遍历链表的方式,时间复杂度是O(n),当一个链表上挂载了大量的元素时,Map的性能就会大打折扣。为了解决这个问题,jdk1.8中当某一个链表上的元素数量大于某个数值(默认为8)时,会将改条链表转为红黑树结构
public V put(K key, V value) {
//调用putVal方法 在此之前会对key做hash处理
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;
//如果当前数组table为null,进行resize()初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//(n - 1) & hash 计算出下标 如果该位置为null 说明没有碰撞就赋值到此位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//反之 说明碰撞了
Node<K,V> e; K k;
//判断 key是否存在 如果存在就覆盖原来的value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//没有存在 判断是不是红黑树
else if (p instanceof TreeNode)
//红黑树是为了防止哈希表碰撞攻击,当链表链长度为8时,及时转成红黑树,提高map的效率
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//都不是 就是链表
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//将next指向新的节点
p.next = newNode(hash, key, value, null);
//这个判断是用来判断是否要转化为红黑树结构
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// key已经存在直接覆盖value
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;
}
5.扩容的过程
当进行addEntry的操作时,如果size>=threshold时,进行扩容操作,扩容操作将table的容量扩大到现在容量的两倍。
这样的扩容方式确保HashMap数组的长度一定是2的次幂,这样既保证了新老数组索引相同,又使数组索引index更加均匀。
6.线程安全问题
若存在线程A和线程B同时往HashMap中进行put操作,在使用哈希函数寻址时,若定位到了数组的同一个位置,可能会出现某个线程的put操作覆盖掉前一个线程put操作的数值,因此HashMap是线程不安全的。
参考文章
https://segmentfault.com/a/1190000017362670
https://segmentfault.com/a/1190000013650892