1.数据结构
一般地,我们讨论的JDK版本为JDK1.7和JDK1.8。
(1)jdk1.7
数组+链表。
(2)jdk1.8
数组+链表+红黑树。
2.HashMap中的哈希算法
(1)什么是哈希:
Hash,一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。
(2)哈希算法:
简单来说,通过哈希表(HASH-TABLE),对散列进行映射,根据键(Key)而直接访问在内存存储位置。
(3)HashMap中的哈希算法:
在jdk1.8中,HashMap中主要是通过index=hash&(n-1)
以此来确定元素在数组中的索引。(此处n为容器长度,且长度必须为2的n次幂,原因可以点击查看这篇文章)
注:“无符号”右移位运算符(>>>),它使用了“零扩展”:无论正负,都在高位插入0。
3.HashMap中的哈希冲突
一般地,我们把不同的key映射到同一个hash值,叫做哈希碰撞,也就是哈希冲突。
HashMap中如何解决哈希冲突:
这里我们仅讨论jdk1.8
首先通过链地址法(使用HASH-TABLE)来链接拥有相同hash值的数据,计算出hashcode
,然后使得hash=hashcode^hashcode>>>16
(这样可以大大减少哈希冲突?请看==>HashMap的扰动函数),将hash
与和(数组长度n-1)
相与,转化为公式index=(n - 1) & hash
计算出索引值index
,此时能够过滤大部分哈希冲突,如果仍然存在哈希冲突则将新的元素插入对应数组索引处的链表尾部(尾插法)。
4.源码分析
(1)put方法
我们来逐步分析:
- 定义容器
tab[node]
- 初始化
当放入第一个元素时,会创建一个默认长度16的链表数组,并设置默认阈值12。
1.第一次放入元素会进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
2.进入resize()方法中,如果没有指定容器长度,容器大小默认值为16
newCap = DEFAULT_INITIAL_CAPACITY;
容器阈值,达到阈值会扩容,默认12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
- 定位索引index
通过index=(n - 1) & hash
计算出索引值index
,index
就是put进来的元素准备投放在数组中的位置索引,然后对tab[index]
进行分类讨论。
if ((p = tab[i = (n - 1) & hash]) == null)
//tab[index]为空,则此时创建一个链表,并放入value值,key值,hash值。
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))))
//该元素为已存在的key,此时进行替换key映射的value。
e = p;
//如果tab[index]为红黑树,进行红黑树相关的插入方法,
//此处较为复杂暂时不展开进行讨论。
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果tab[index]为链表,则遍历此链表
for (int binCount = 0; ; ++binCount) {
//判断p.next来确认是否到达链表尾部
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;
}
//此处进行判断是否插入的是相同的key
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//找到后e不为空,该元素为已存在的key,此时进行替换key映射的value。
break;
//因为要循环遍历,所以需要不断赋值找到下一个节点
p = e;
}
}
//该元素为已存在的key,此时进行替换key映射的value。
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
- 链表红黑树互相转化条件
(1) 当链表长度大于等于8时,此时会将链表转化为红黑树。
(2) 当红黑树长度小于等于6时,又转化为链表结构
(3) 作为缓冲,当链表(红黑树)长度为7时保持当前数据结构不变 - 扩容
if (++size > threshold)
resize();
如果tab长度达到阈值,将扩容,具体如何扩容还要继续进行分类讨论。
如果能够大致确定tab的长度,则尽量初始化设置相近的长度,因为扩容很消耗性能。
==>详细请看这篇文章【HashMap的扩容机制—resize()】