|
|
|
|
## 一、基础
|
|
|
|
|
散列表:数据的存储位置与关键字之间存在对应关系
|
|
|
|
|
通过计算key的hash值来计算值的存储位置,hashmap是数组和链表组成的,数据结构中又叫“链表散列”。
|
|
|
|
|
![[20190423204113985.png]]
|
|
|
|
|
|
|
|
|
|
![[Snipaste_2023-02-09_16-29-10.png]]
|
|
|
|
|
|
|
|
|
|
### hash冲突
|
|
|
|
|
|
|
|
|
|
因为不同的key计算得到的hash值可能会相同,此时就产生了hash冲突,单向链表就是用于解决hash冲突。当得到相同hash值的元素时,将其存入next结点。
|
|
|
|
|
解决hash冲突可以通过开地址法或拉链法解决,单向链表是拉链法的实现之一。
|
|
|
|
|
开地址法:有冲突时就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将数据元素放入。(具体看B站:站长数据结构)
|
|
|
|
|
|
|
|
|
|
**由于单链表达到一定长度后查找效率低(单链表需要从头开始遍历),因此在JDK1.8中当单链表长度超过8,并且数组长度大于64后会变成红黑树**
|
|
|
|
|
|
|
|
|
|
### hash函数
|
|
|
|
|
在Java中所有对象都有hashcode,因此计算可以通过key的hashcode进行下标计算
|
|
|
|
|
下标 = hash%16,其中16是因为数组默认大小是16,这样可以确保算出来的值足够随机。
|
|
|
|
|
|
|
|
|
|
### hashmap的扩容原理
|
|
|
|
|
初始容量大小和加载因子,初始容量大小是创建时给数组分配的容量大小,默认值为16,加载因子默认0.75f,用数组容量大小乘以加载因子得到一个值,一旦数组中存储的元素个数超过该值就会调用rehash方法将数组容量增加到原来的两倍
|
|
|
|
|
在做扩容的时候会生成一个新的数组,原来的所有数据需要重新计算哈希码值重新分配到新的数组,所以扩容的操作非常消耗性能
|
|
|
|
|
HashMap默认采用数组+单链表方式存储元素,当元素出现哈希冲突时,会存储到该位置的单链表中。但是单链表不会一直增加元素,当元素个数超过8个时,会尝试将单链表转化为红黑树存储。但是在转化前,会再判断一次当前数组的长度,只有数组长度大于64才处理。否则,进行扩容操作。
|
|
|
|
|
|
|
|
|
|
`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)
|
|
|
|
|
n = (tab = resize()).length;
|
|
|
|
|
if ((p = tab[i = (n - 1) & hash]) == null)
|
|
|
|
|
tab[i] = newNode(hash, key, value, null);
|
|
|
|
|
else {
|
|
|
|
|
Node<K,V> e; K k;
|
|
|
|
|
if (p.hash == hash &&
|
|
|
|
|
((k = p.key) == key || (key != null && key.equals(k))))
|
|
|
|
|
e = p;
|
|
|
|
|
else if (p instanceof TreeNode)
|
|
|
|
|
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
|
|
|
|
|
else {
|
|
|
|
|
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;
|
|
|
|
|
if (++size > threshold)
|
|
|
|
|
//扩容
|
|
|
|
|
resize();
|
|
|
|
|
afterNodeInsertion(evict);
|
|
|
|
|
return null;}`
|
|
|
|
|
|
|
|
|
|
## 二、特点
|
|
|
|
|
### 1.为什么HashMap要树化
|
|
|
|
|
之前在极客时间的专栏里看到过一个解释。本质上这是个安全问题。因为在元素放置过程中,如果一个对象哈希冲突,都被放置到同一个桶里,则会形成一个链表,我们知道链表查询是线性的,会严重影响存取的性能。而在现实世界,构造哈希冲突的数据并不是非常复杂的事情,恶意代码就可以利用这些数据大量与服务器端交互,导致服务器端CPU大量占用,这就构成了哈希碰撞拒绝服务攻击,国内一线互联网公司就发生过类似攻击事件。
|
|
|
|
|
|
|
|
|
|
### 2.为什么要将链表中转红黑树的阈值设为8
|
|
|
|
|
|
|
|
|
|
我们可以这么来看,当链表长度大于或等于阈值(默认为 8)的时候,如果同时还满足容量大于或等于 MIN_TREEIFY_CAPACITY(默认为 64)的要求,就会把链表转换为红黑树。同样,后续如果由于删除或者其他原因调整了大小,当红黑树的节点小于或等于 6 个以后,又会恢复为链表形态。
|
|
|
|
|
|
|
|
|
|
每次遍历一个链表,平均查找的时间复杂度是 O(n),n 是链表的长度。红黑树有和链表不一样的查找性能,由于红黑树有自平衡的特点,可以防止不平衡情况的发生,所以可以始终将查找的时间复杂度控制在 O(log(n))。最初链表还不是很长,所以可能 O(n) 和 O(log(n)) 的区别不大,但是如果链表越来越长,那么这种区别便会有所体现。所以为了提升查找性能,需要把链表转化为红黑树的形式。
|
|
|
|
|
|
|
|
|
|
通常如果 hash 算法正常的话,那么链表的长度也不会很长,那么红黑树也不会带来明显的查询时间上的优势,反而会增加空间负担。所以通常情况下,并没有必要转为红黑树,所以就选择了概率非常小,小于千万分之一概率,也就是长度为 8 的概率,把长度 8 作为转化的默认阈值。
|