You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

84 lines
6.0 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## 一、基础
散列表:数据的存储位置与关键字之间存在对应关系
通过计算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 作为转化的默认阈值。
### 3.加载因子为什么0.75
对于使用链表法的散列表来说,查找一个元素的平均时间是 O(1+a)。因此如果负载因子越大对空间的利用更充分然而后果是查找效率的降低如果负载因子太小那么散列表的数据将过于稀疏对空间造成严重浪费。0.75是HashMap在时间和空间两者间折中选择通过测试后符合泊松分布。