一、HashMap底层实现结构
在JDK1.7以前,HashMap的底层数据结构的实现是数组 + 链表的实现方式。但是在1.8之后HashMap的实现是数组 + 链表 + 红黑树
在理解Hash和扩容流程之前,我们得先了解下HashMap的几个字段:
int threshold; // 所能容纳的key-value对极限
final float loadFactor; // 负载因⼦
int modCount;
int size;
loadFactor:负载因子
默认为0.75 ,是决定扩容阈值的重要因素之一
threshold:扩容的阈值
扩容阈值的计算公式:threshold = length * LoadFactor,length为数组的长度
threshold就是在此loadFactor和length(数组长度)对应下允许的最大元 素数目,超过这个数目就重新resize(扩容) ,扩容后的HashMap容量是之前容量的两倍
modCount:记录内部结构发生变化的次数
内部结构发生变化指的是结构发生变化,例如put新键值 对,但是某个key对应的value值被覆盖不属于结构变化
size:HashMap中存在的键值对数量
注意和table的长度length 、容纳 最大键值对数量threshold的区别
这里存在一个问题,即使负载因子和Hash算法设计的再合理,也免不了会出现拉链过长的情况,一旦出 现拉链过长,则会严重影响HashMap的性能。于是,在JDK1.8版本中,对数据结构做了进一步的优 化,引入了红黑树。而当链表长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增删 改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。
二、确定哈希桶数组索引位置
不管增加、删除、查找键值对,定位到哈希桶数组的位置都是很关键的第一步。前面说过HashMap的 数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量分布均匀些,尽 量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道 对应位置的元素就是我们要的,不用遍历链表,大大优化了查询的效率。 HashMap定位数组索引位 置,直接决定了hash方法的离散性能。
⽅法⼀:
static final int hash(Object key) { //jdk1.8 & jdk1.7
int h;
// h = key.hashCode() 为第⼀步 取hashCode值
// h ^ (h >>> 16) 为第⼆步 ⾼位参与运算
// 1001 0111 0011 0101 1101 1110 1001 1111 => hash code
// 0000 0000 0000 0000 1001 0111 0011 0101 => 右移 16 位
// 1001 0111 0011 0101 0100 1001 1010 1010 => 异或运算
// 让同一个 key 的高位与地位都参与 hash 运算
// 目的: 降低 hash 碰撞的概率
// 最终返回一个 异或运算 后的一个更混乱的 hash 值
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
⽅法⼆:
static int indexFor(int h, int length) { //jdk1.7的源码,jdk1.8没有这个⽅法,但是实现原理⼀样的
return h & (length-1); //第三步 取模运算
}
三、HashMap的put方法
public V put(K key, V value) {
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 数组 => hash 表, 额外的变量
Node<K, V> p; // 当前 key 存放索引位置的元素
int n, // table 数组的长度
i; // 当前 key 经过运算后的索引位置 => (长度 - 1) & hash
// 如果 table 数组为 null 或数组的长度为 0, 就进行扩容 => 初始化
if ((tab = table) == null || (n = tab.length) == 0)
// 第一次扩容实际就是 table 的初始化
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) // 说明索引位置没有值
// 如果该索引位置没有值, 就直接创建一个新的节点, 并将其存在这个位置上
tab[i] = newNode(hash, key, value, null);
else { // 如果这个索引位置已经有值
Node<K, V> e; // 上一个相同 key 的节点对象, 与本次存入的新的 key 完全相同的 Node 对象
K k; // 当前索引位置上的 key
// 判断当前索引位置的元素与新存入的 key 是否完全相同
// 第一种情况: 直接拿头节点进行比较, 不关心他是树节点, 还是链表节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 如果这个位置上的 key 与即将存入的 key 完全相同, 就直接将当前节点赋值给 e
e = p;
// 第二种情况: 判断是否是树节点, 也就是判断当前节点是否已经发生过 hash 冲突, 且已经变成红黑树
else if (p instanceof TreeNode) // 判断当前节点是否已经变成树节点 => 判断是否已经变成红黑树
// 往红黑树中存入当前新的 key/value, 并返回存入后的对象赋值给 e
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
else {
// 第三种情况: 如果以上两者都不是, 就默认为链表, 判断是否有下一节点, 进行循环判断
for (int binCount = 0; ; ++binCount) { // 计数, 计算链表的长度
// 判断当前节点是否有下一个节点, 如果为 null
if ((e = p.next) == null) {
// 将即将存入的 key/value 存入一个新的节点, 并设置到当前节点的 next
p.next = newNode(hash, key, value, null);
// 判断当前链表长度是否大于树化的阈值(7)
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 链表长度 >= 8, 那么就准备进行转成树
treeifyBin(tab, hash);
break; // 这次循环结束
}
// 此时 e = 当前正在遍历的链表元素
// 判断该元素的 hash 与 key 是否与即将存入的 key 完全相同
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break; // 如果相同, 直接结束循环
p = e; // 将当前遍历的节点, 赋值给之前索引位置的节点
}
}
// 如果找到了与本次要存入的 key 完全相同的节点, 直接将本次存入新的 value 替换旧节点的 value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) // 如果配置了只有在不存在时才存入 或 原来的值为 空
e.value = value; // 将新的 value 覆盖原来旧的值
afterNodeAccess(e);
return oldValue;
}
}
++modCount; // 并发修改数的记录
if (++size > threshold) // 存入键值对的数量+1, 且判断是否大于扩容阈值
resize(); // 扩容
afterNodeInsertion(evict);
return null;
}
四、HashMap的扩容原理
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;//oidCap原数组长度
int oldThr = threshold;//threshold:数组使用超过threshold,扩容
int newCap, newThr = 0;//newCap:新数组长度,newThr:新数组threshold
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {//MAXIMUM_CAPACITY=1<<30:最大长度
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&//扩容一倍
oldCap >= DEFAULT_INITIAL_CAPACITY)
//DEFAULT_INITIAL_CAPACITY=16:默认长度
newThr = oldThr << 1;
}
else if (oldThr > 0)
newCap = oldThr;
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
//DEFAULT_LOAD_FACTOR=0.75
}
if (newThr == 0) {
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
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) {
//当容量扩充一倍时,同一索引下的链表中的节点在新数组中可能会出现两种变化,1:对应的索引不变
//2:对应的索引+原长度
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//将位置不变的节点组成一个链表,lohead为他们的第一个节点,尾节点为lotail
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
//将位置变为原位置+原长度的节点组成一个链表,头节点为hihead,尾节点为hitail
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
//原数组原位置最后一个节点要么为不变位置的节点,要么为变位置的节点,
//lonTail.next和hitail.next总有一个不为空
}
}
}
}
return newTab;
}