手撕 JDK1.8 HashMap源码(下)
手撕源码
1. 核心常量与属性
常量
// table初始容量大小 默认值是16 且容量一定是2的次数
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// table最大长度 2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
// 负载因子大小 默认值是0.75 当哈希表使用了75%就会扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 树化条件1 树化阈值 链表长度达到8
static final int TREEIFY_THRESHOLD = 8;
// 树化条件2 table的长度达到64 满足该条件且满足树化条件1 链表才会树化
static final int MIN_TREEIFY_CAPACITY = 64;
// 树降级为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
Node类
// put到Map里面的数据都会被封装为一个Node<K,V>存放到散列表当中
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // K.hash()经过一次扰动
final K key;
V value;
Node<K,V> next; // hash碰撞 链表的next指针
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() {
return key; }
public final V getValue() {
return value; }
public final String toString() {
return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// equals()
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
属性
// 哈希表table 由Node数组构建 只有第一次put操作时才会初始化table 默认长度是16
transient Node<K,V>[] table;
// 每一个Node构成的一个Set集合
transient Set<Map.Entry<K,V>> entrySet;
// 当前哈希表元素个数
transient int size;
// 当前哈希表结构修改次数 只有增删元素才会改变结构 替换元素不会使modCount变化
transient int modCount;
// 扩容阈值 当哈希表中的元素个数超过(数组长度 * 负载因子)时 就会扩容
int threshold;
// 负载因子 默认值是0.75
final float loadFactor;
2. 构造方法
双参
// 传入一个初始化大小和负载因子
public HashMap(int initialCapacity, float loadFactor) {
// 这3个if就是对参数进行校验
if (initialCapacity < 0) // 如果为负数则抛出异常
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 如果大于最大值 则还原为最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 如果<=0或者非数 则抛出异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor; // 赋值
// 传进来的initialCapacity可能是5,7这些值 需要利用tableSizeFor()确保大小是合法的 往下看该方法解析
this.threshold = tableSizeFor(initialCapacity);
/* ↓
作用:通过进制计算 返回一个大于等于当前值cap的一个数字 并且这个数字一定是2的次方。详见下方对于cap为10的计算
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
cap = 10;
n = 10 - 1 => 9
n >>> x 表示n在2进制中右移x位
00001001 | 00000100 => 00001101
00001101 | 00000011 => 00001111
00001111 | 00000000 => 00001111
00001111 | 00000000 => 00001111
00001111 | 00000000 => 00001111
00001111(2) => 15(10)
return 15 + 1 => 16
*/
}
单参
// 传入初始化大小
public HashMap(int initialCapacity) {
// 这里的this其实是套娃了上面的双参构造方法
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
无参
// 最常用的构造方法 无参 将负载因子初始化为默认的0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
Map为参数
// 将一个Map中数据复制到新的Map中
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
3. put() => putVal()
put()
public V put(K key, V value) {
// 通过hash()计算key的哈希值 再把hash,key,value传给putval()方法
return putVal(hash(key), key, value, false, true);
/* ↓
这个hash()就是我们上面提到的扰动函数
作用:让key的hash值的高16位也参与路由运算。
static final int hash(Object key) {
int h;
// 当key为null时 hash值为0 放到table下标为0的位置 不为null则对key的hashCode进行^异或运算 详见下方
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
^ 异或:相同返回0,不同返回1
h = 0b 0010 0101 1010 1100 0011 1111 0010 1110
0b 0010 0101 1010 1100 0011 1111 0010 1110
^
0b 0000 0000 0000 0000 0010 0101 1010 1100 右移16位
=> 0010 0101 1010 1100 0001 1010 1000 0010
一般我们的length会比较小 但是hashcode范围是很大的 这样的话key的hashCode的高位就没有参数到运算中,容易造成哈希冲突
如果要让高16位也参与进路由寻址 需要进行一些运算,在这里也就是^异或运算 让高16位的特征也能参与到低16位中
让高16位也参与运算,可以使哈希值更加散列,可以减小哈希冲突。
*/
}
putvalue()
我们可以根据图片来理解代码:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// tab 引用当前的hashMap的散列表
// p 表示散列表当前位置的元素
// n 表示散列表数组的长度
// i 表示路由寻址得到的结果(下标)
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 成员属性table赋值给局部变量tab判断是否为null 或者是将tab的长度赋值给n判断是否为0 表示当前hashMap中还没有创建table
if ((tab = table) == null || (n = tab.length) == 0)
// 延迟初始化:第一次调用putval()方法 才会调用resize()扩容方法 创建散列表
// 有时候我们把hashMap new出来之后 并不往里面放数据 如果我new的时候就把最占内存的散列表创建出来 那就会浪费内存了
// 调用resize()扩容算法 创建散列表 再把新扩容的长度赋值给n
n = (tab = resize()).length;
// 计算hash值的下标 (n - 1) & hash => 路由算法
// &运算 对二进制进行运算(会计算的快一些)其实就是取余%操作 hash % (n - 1) 得到下标赋值给i
if ((p = tab[i = (n - 1) & hash]) == null)
// 情况1.如果当前寻址找到的桶位为null 则new一个新链表结点 k,v=>node 直接扔到下标为i的散列表tab
tab[i] = newNode(hash, key, value, null);
else {
// 如果值已经存在
// e:不为null的话,则找到了一个与当前要插入的key-value一致的key的元素node
// k:临时的一个key
Node<K,V> e; K k;
// 情况2.表示桶位中的元素 与你当前插入的元素key完全一致,表示后续需要进行覆盖操作
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; // 将p赋值给e
// 情况3.此时已经树化
else if (p instanceof TreeNode)
// 按照红黑树的结构去存 如果存储时发生了覆盖 则赋值给e
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 情况4.此时还是链表结构 而且链表的头元素与我们要插入的key不一致
else {
// 遍历链表里的所有数据
for (int binCount = 0; ; ++binCount) {
// 当迭代到null时 说明已在链表的末尾 且在该链表也没有找到一个与你插入的key一致的node元素
if ((e = p.next) == null) {
// 将该结点添加到p.next位置 也就是链表的末尾
p.next = newNode(hash, key, value, null);
// 此时需要进行判断 未插入p时的结点数是否已经达到了8个
// 如果已经达到了8个 那么插入p.next时链表长度就变成了9 > 8 就需要对链表进行树化操作
// 循环从0开始 binCount >= TREEIFY_THRESHOLD - 1 => 7 此时链表已经有8个元素
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash); // 树化操作 后期会更新红黑树的文章
break;
}
// 此时e.hash也就是p.next.hash与当前插入的hash相同 且key一致 说明找到了一个相同元素 则break出外面操作
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; // 一步一步往后迭代
}
}
// 条件成立说明 找到了一个与你插入元素key完全一致的数据 需要执行覆盖操作 e赋上了值就代表要覆盖 e不赋值就代表存上了
if (e != null) {
// existing mapping for key:密钥的现有映射
V oldValue = e.value; // 先取出旧值
// !onlyIfAbsent = true
if (!onlyIfAbsent || oldValue == null)
e.value = value; // 然后把现在要存的值赋给e.value 新值替换旧值
afterNodeAccess(e); // 访问后回调
return oldValue; // 把旧的值返回 不执行后面的操作
}
}
++modCount; // 表示散列表被修改的次数 插入新元素才会++ 替换在上面直接return了 不++
// ++size后 大于扩容阈值 触发扩容算法
if (++size > threshold)
resize(); // 扩容算法
afterNodeInsertion(evict); // 插入后回调
return null;
}
put()
方法总结
先通过hash()
扰动计算出哈希值,然后调用putVal()
方法:
- 延迟初始化:先判断哈希表是否创建过,如果未创建则创建
table
- 通过路由算法
(n - 1) & hash
寻址得到当前元素的下标 - 插入元素
- ① 当前桶位为空,直接放
- ② 当前位置有元素,且哈希值相同,key一致,则进行赋值,后续进行
value
的覆盖操作 - ③ 当前位置已被树化,如果存储时发生了覆盖,则赋值,否则插入到树中
- ④ 当前位置是链表结构,遍历整个链表,如果找到哈希值相同,且key一致,后续进行
value
的覆盖操作,否则插入到链表尾部,并判断当前链表长度是否达到了树化标准(链表长度达到8 并且数组长度达到64(这个判断条件在树化的treeifyBin()方法中))
- 覆盖
value
操作 - 插入元素后 更新变量,
++modCount
,判断++size
是否超过当前大小,如果超过则resize()
扩容。
4. resize() 扩容(核心)
// 为什么需要扩容?为了解决哈希冲突导致的链化影响查询效率问题,扩容会缓解该问题。
final Node<K,V>[] resize() {
// oldTab:引用扩容前的哈希表
Node<K,V>[] oldTab = table;
// oldCap:表示扩容之前table数组的长度 如果是null 则长度为0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// oldThr:扩容之前的扩容阈值(数组长度),触发本次的扩容阈值
int oldThr = threshold;
// newCap:扩容之后达到的大小
// newThr:扩容之后,下次再次触发扩容的条件
int newCap, newThr = 0;
// 条件成立 说明hashMap中的散列表已经初始化过了,是一次正常扩容
if (oldCap > 0) {
// 扩容之前的table数组大小已经达到最大阈值后,则不扩容,且设置扩容条件为 int 最大值
if (oldCap >= MAXIMUM_CAPACITY) {
// 生成这个int最大值就很难达到这个扩容阈值了 但这种情况很少见
threshold = Integer.MAX_VALUE;
return oldTab;
}
// oldCap左移一位 实现数值翻倍 并赋值给newCap,newCap小于数组最大值限制 且 扩容之前的阈值 >= 16
// 这种情况下,则下一次扩容的阈值 等于当前阈值翻倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold:双倍阈值
}
// oldCap == 0 说明hashMap中的散列表是null
// 1.new HashMap(initCap, loadFactor);
// 2.new HashMap(initCap);
// 3.new HashMap(map); 并且这个map有数据
// 在这3中构造方法情况下,oldThr有值
else if (oldThr > 0) // initial capacity was placed in threshold:初始容量设置为阈值
// 第一次构建 直接赋值给newCap
newCap = oldThr;
// oldCap == 0,oldThr == 0
// new HashMap();
// 只有调用的是无参构造方法时
else {
// zero initial threshold signifies using defaults:零初始阈值表示使用默认值
newCap = DEFAULT_INITIAL_CAPACITY; // 16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 0.75 * 16 => 12
}
// newThr为零时,通过newCap和loadFactor计算出一个newThr
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
// 判断newCap和ft是否合法 如果合法则将计算出的ft转为int 不合法则赋予int最大值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr; // 更新下一次的扩容阈值
@SuppressWarnings({
"rawtypes","unchecked"}) // 注解作用:抑制编译器产生的多类型警告信息
// 根据newCap创建出一个更长的数组 也有可能是第一次创建数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab; // 将新创建的node赋值给table
// 说明,hashMap本次扩容之前 table不为null
// 扩容操作
if (oldTab != null) {
// 将每个旧哈希表中的数据都移动到新的散列表中
// 遍历旧哈希表中的每一个桶,重新计算桶里元素在新哈希表中的位置
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e; // 当前node节点
// 说明当前桶位中有数据 但是数据具体是单个数据/链表/红黑树 暂时还未知
if ((e = oldTab[j]) != null) {
// 原来的数据赋值为null 方便JVM GC时回收内存
oldTab[j] = null;
// 情况1.说明该桶中未发生过哈希碰撞 只有一个元素
if (e.next == null)
// 根据新length通过路由寻址算法得到新下标 插入到新位置
newTab[e.hash & (newCap - 1)] = e;
// 情况2.当前节点已树化
else if (e instanceof TreeNode)
// 红黑树处理 暂且跳过 后期会更新红黑树的文章
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 情况3.当前节点为链表 下方有图
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;
/*
假设当前oldCap 也就是当前哈希表长度为16 => 0b 10000
且当前结点存放在下标为15的位置 则该节点哈希值的后4位一定是1111 第5位可能是0也可能是1
e.hash -> ... 0 1111
e.hash -> ... 1 1111
e.hash & oldCap == 0 说明该节点的下标为15 即放在原下标位置(低位链表)
e.hash & oldCap == 16 => != 0 说明该节点的下标为31 即放在新下标位置(高位链表)
*/
// hash值 & 原长度 == 0 说明元素应该存至低位链表
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; // 放到 原位置 + 扩容之前的table长度
}
}
}
}
return newTab; // 返回新散列表
}
扩容—链表处理
低位链表:存放在扩容之后的数组的下标位置 与当前数组的下标一致(蓝色)
高位链表:存放在扩容之后的数组的下标位置为 当前数组下标位置 + 扩容之前数组的长度(绿色)
resize()扩容方法总结
- 获取原哈希表的数据 并进行验证和计算后 得到新值
newCap
(哈希表的新长度),newThr
(哈希表的新扩容阈值) - 根据
newCap
创建出新的哈希表 - 扩容操作
- ① 该桶中只有一个元素,直接通过路由寻址算法得到新下标 插入到新位置
- ② 当前节点已树化,由树化操作寻址插入
- ③ 当前节点为链表结构,分离链表(低位、高位),根据位运算判断当前节点应存至低位/高位
- 存放操作
5. get() => getNode()
get()
public V get(Object key) {
Node<K,V> e;
// put经过hash(),取时肯定也要hash() 如果key为空则返回空 否则返回value
return (e = getNode(hash(key), key)) == null ? null : e.value;
} // ↓
getNode()
final Node<K,V> getNode(int hash, Object key) {
// tab:引用当前hashMap的散列表
// first:桶位中的头元素
// e:临时node元素
// n:table数组长度
// k:key
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 如果哈希表的table存在 且桶中的第一个元素存在 但并不知道该元素是单个/链表/红黑树
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 情况1.定位的桶位首元素 即为我们要get的数据 直接把该元素返回
if (first.hash == hash && // always check first node:始终检查第一个节点
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 当前桶位不是单个元素 可能是链表/红黑树
if ((e = first.next) != null) {
// 情况2.桶位是红黑树结构 在树中寻找
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 情况3.桶位是链表结构 遍历链表寻找
do {
// 找到
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null; // 没有数据直接返回空
}
6. remove() => removeNode()
remove()
单参
// 根据key删除
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
双参
// 根据key和value删除
@Override
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
removeNode()
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
// matchValue参数 如果为true则需要对value也进行比较 参照上面的两个构造方法 只传key时 该参数为false
// tab:引用当前hashMap中的散列表
// p:当前node元素
// n:表示散列表数组长度
// index:表示寻址结果
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 判断table中是否有元素 如果有则计算出当前元素位置并赋值给p
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
// node:查找到的结果
// e:当前Node的下一个元素
Node<K,V> node = null, e; K k; V v;
// 进行查找操作 并删除
// 情况1.当前桶位中的元素 即为你要删除的元素
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// p.next不为空 当前桶位可能是链表/红黑树
else if ((e = p.next) != null) {
// 情况2.当前桶位已是红黑树
if (p instanceof TreeNode)
// 红黑树的查找操作
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
// 情况3.当前桶位为链表
else {
// 遍历整个链表 查找元素
do {
// 找到 赋值给node
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 对比查找到的key和value是否与传进来的匹配
// 删除操作
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 情况1.如果node是红黑树结构 则用树的操作删除
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 情况2.当前桶位即为要删除的元素
else if (node == p)
// 直接将当前结点指向next 即删除
tab[index] = node.next;
// 情况3.将当前元素p的下一个元素 设置成 要删除元素的 下一个元素
else
p.next = node.next;
++modCount; // 更新修改次数
--size; // 更新元素个数
afterNodeRemoval(node);
return node; // 返回结果
}
}
return null; // 如果查询不到 则返回null
}
7. replace()
三参
@Override
public boolean replace(K key, V oldValue, V newValue) {
Node<K,V> e; V v;
// 根据key找到node结点 判断找到的value跟传进来的oldValue是否一致 如果一致则用新的newValue替换value
if ((e = getNode(hash(key), key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
e.value = newValue;
afterNodeAccess(e);
return true;
}
return false;
}
双参
@Override
public V replace(K key, V value) {
Node<K,V> e;
// 根据key找到node结点 做替换 把旧值返回
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
据key找到node结点 判断找到的value跟传进来的oldValue是否一致 如果一致则用新的newValue替换value
if ((e = getNode(hash(key), key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
e.value = newValue;
afterNodeAccess(e);
return true;
}
return false;
}
文章参考:b站小刘讲源码视频