什么是哈希表
哈希表是数组的一种扩展,它利用的是数组支持按下标直接查找的特性。没有数组就没有哈希表。
数组是一串连续的内存空间:
在使用数组的时候,可以按下标直接得到内存地址值,这样就可以直接得到想要的数据,所以数组在使用下标访问时算法复杂度位O(1)。
在这种高效的查找方式基础上诞生了哈希表:
在我们往哈希表中存储数据的时候,通过哈希函数把key值映射未数组下标,然后将value存储在对应下标的数组空间里。这样在访问时,就可以hash(key)获取到对应下标,从对应的数组下标的位置取数据。实现O(1)的算法复杂度。
- 哈希表的弊端:
在哈希表中有个很重要的算法,就是哈希函数。哈希函数最重要的功能就是:1.相同的key得到的哈希值是一样的。2.不同的key得到哈希值不同。
但是就目前的算法能都会存在哈希冲突,就是不同的key也可能得到相同的哈希值。
即便像业界著名的MD5、SHA、CRC等哈希算法,也无法完全避免这种散列冲突。而且,因为数组的存储空间有限,也会加大散列冲突的概率。所以我们几乎无法找到一个完美的无冲突的散列函数,即便能找到,付出的时间成本、计算成本也是很大的,所以针对散列冲突问题,我们需要通过其他途径来解决。
HashMap
因为哈希表会存在冲突,所以针对冲突的解决诞生了新的数据结构:数组+链表
每个数组下标都对应一个桶(bucket),冲突的key都落到同一个桶中,每个bucket中得数据使用一个链表进行存储,链表节点存储Entry,每个Entry包含键值对(key、value)和下一个节点的地址(next)。当查询的时候,会计算key对应的数组下标,找到对应桶,然后遍历桶对应的链表,比较key值,最后获得数据。
以上分析了HashMap的主要结构模型,下面我们来结合源码来看看HashMap是怎么实现初始化、扩容以及put和get的。
源码分析
在进行源码分析前我们先看几个HashMap重要的变量:
- size:表示HashMap中存储键值对(KV)的数量.
- capacity:指的HashMap中桶的数量,也就是数组的长度。默认值为16,会随着数据量的增大而扩容。
- loadFactor:负载因子。负载因子是用来衡量HashMap满的程度,其默认值为0.75f。计算方法为size除以capacity。
- threshold:阈值,当HashMap的size大于threshold时会执行扩容(resize)操作。(capacity * loadFactor)
初始化与扩容
我们来看看put的源码:
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)
//初始化map容量
n = (tab = resize()).length;
继续看resize()方法:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//判断容量是否大于最大容量(1 << 30)
//大于等于时则无法扩容,返回旧的table
if (oldCap >= MAXIMUM_CAPACITY) {
//将阈值设置为最大int值
threshold = Integer.MAX_VALUE;
return oldTab;
}
//能扩容,(oldCap << 1)也就是乘以2,
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//阈值乘以2
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else {
// zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
//默认负载因子*默认容量
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
//新的capacity*loadFactor
float ft = (float)newCap * loadFactor;
//如果最大容量大于新的且计算值小于最大容量,则使用新的阈值。
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({
"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//以下是扩容的数据拷贝逻辑
if (oldTab != null) {
//循环遍历
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//没有下个节点则直接计算下标并拷贝
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果是红黑树,则进行红黑树拷贝
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//这里则为链表操作
else {
// preserve order
//使用两个链表,loHead和loTail链表放入下标和旧数组相同的下标桶中
//loHead和loTail链表放入下标为 旧数组所在下标+旧容量 的下标桶中
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
//新桶下标 = 旧桶下标
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
//旧桶下标+旧容量 = 新桶下标
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
看完resize()方法后我们再结合下图就能很顺畅的了解HashMap的扩容机制了:
在JDK1.8中,当链表长度大于8的时候,链表会转化为红黑树以提升查询效率。
put方法
我们来看下源码:
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;
//判断桶中是否有数据,没有则直接创建一个node放入桶中
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//桶中有数据,则追加链表或者红黑树
else {
//e:已经存在的
Node<K,V> e; K k;
//判断当前node是否匹配传入的key,匹配则替换。不是则往下查询
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) {
//将e指向下一个节点,下个节点为null,追加链表
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//判断链表长度是否达到8,达到则转换为红黑树存储
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//判断当前节点是否为put的节点,是则结束遍历。
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;
//判断size是否大于阈值,大于触发扩容操作
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
看完源码,我们再来总结下流程:
总结
HashMap主要的数据结构、初始化和扩容及put流程都分析完了,其中关于红黑树的部分没有做详细分析,因为红黑树本身旧比较复杂,后面会将红黑树单独来一波分析。