HashMap源码解析
基础入门
什么是哈希
核心理论:Hash 也称散列、哈希,对应的英文都是 Hash。基本原理就是把任意长度
的输入,通过Hash算法变为固定长度
输出。
这个映射的规则就是对应的 Hash 算法,而原始数据映射后的二进制串
就是哈希值。
Hash 的特点:
-
从 hash 值不可以
反向推导
出原始的数据 -
输入数据的
微小变化
会得到完全不同的 hash 值,相同的数据会得到相同的值 -
哈希算法的执行效率要
高效
,长的文本也能快速的计算出哈希值 -
hash 算法的冲突概率要小
由于 hash 的原理是将输入空间
的值映射成hash空间
内,而 hash 值的空间远小于输入的空间。
根据抽屉原理
,一定会存在不同的输入被映射成相同输出的情况。
抽屉原理:桌子上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们发现至少会有一个抽屉里面放不少于两个的苹果,这一现象就是我们所说的“抽屉原理”。
HashMap原理讲解
1. HashMap的继承体系
// HashMap继承AbstractMap
public class HashMap<K,V> extends AbstractMap<K,V>
// AbstractMap实现Map
public abstract class AbstractMap<K,V> implements Map<K,V>
2. 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指针
3. 底层存储结构介绍
jdk1.8
之前,由 数组+链表 组成。
jdk1.8
开始,由 数组+链表+红黑树 组成。
4. put数据原理分析
5. 什么是Hash碰撞
进行路由寻址之后计算的下标index
相同,即出现Hash碰撞
。
6. 什么是链化
由于Hash碰撞
,从而形成链表
。
7. jdk8为什么引入红黑树
链化非常严重,链表过长,查询时间复杂度由
O
(
1
)
→
O
(
N
)
O(1)→O(N)
O(1)→O(N),故引入红黑树
解决。
8. HashMap扩容原理
空间换时间
负载因子
0.75
负载因子指的是什么呢?当我们要存 10000 个数据时,每一个哈希桶都会存好几百个数据,那么每一个哈希桶的链表长度都很长,它的性能会变得很低,即使它会优化成红黑树;负载因子 0.75 的含义就是当我们所有的桶已经有 75% 的桶都存上数据时,会对桶的数量进行扩容,桶会变得更多,它的扩容大小就是原长度的 2 倍,默认是 16,从 16 扩容成了 32,当然取余操作也要变成 % 32。
经官方测试 负载因子的 0.75 就是最佳值,如果负载因子是 0.5,那么你的哈希桶永远只能存一半,但是它的占用空间大,效率高;如果给到 0.99,那么这个哈希桶空间浪费的特别少,但是找数据就会比较慢。
API文档中的叙述:
手撕源码
核心常量与属性
常量
// 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;
属性
// 哈希表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;
内部类Node
- Node 是一个典型的单链表节点,其中,hash 用来存储 key 计算得来的 hash 值。
// 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;
}
}
内部类TreeNode
- 这是一个神奇的类,它继承自 LinkedHashMap 中的 Entry 类,关于 LinkedHashMap.Entry 这个类我们后面再讲。
- TreeNode 是一个典型的树型节点,其中,prev 是链表中的节点,用于在删除元素的时候可以快速找到它的前置节点。
// 位于HashMap中
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
// ...
// 这个内部类的内容非常多
}
// 位于LinkedHashMap中,典型的双向链表节点
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
构造方法
双参
// 传入一个初始化大小和负载因子
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);
}
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); // 访问后回调 后续LinkedHashMap源码解析的文章会分析这个钩子方法
return oldValue; // 把旧的值返回 不执行后面的操作
}
}
++modCount; // 表示散列表被修改的次数 插入新元素才会++ 替换在上面直接return了 不++
// ++size后 大于扩容阈值 触发扩容算法
if (++size > threshold)
resize(); // 扩容算法
afterNodeInsertion(evict); // 插入后回调 后续LinkedHashMap源码解析的文章会分析这个钩子方法
return null;
}
put()
方法总结
先通过hash()
扰动计算出哈希值,然后调用 putVal()
方法:
- 延迟初始化:先判断哈希表是否创建过,如果未创建则创建
table
- 通过路由算法
(n - 1) & hash
寻址得到当前元素的下标 - 插入元素
- ① 当前桶位为空,直接放
- ② 当前位置有元素,且哈希值相同,key一致,则进行赋值,后续进行
value
的覆盖操作 - ③ 当前位置已被树化,如果存储时发生了覆盖,则赋值,否则插入到树中
- ④ 当前位置是链表结构,遍历整个链表,如果找到哈希值相同,且key一致,后续进行
value
的覆盖操作,否则插入到链表尾部,并判断当前链表长度是否达到了树化标准(链表长度达到8 并且数组长度达到64(这个判断条件在树化的treeifyBin()方法中))
- 覆盖
value
操作 - 插入元素后 更新变量,
++modCount
,判断++size
是否超过当前大小,如果超过则resize()
扩容。
TreeNode.putTreeVal()
- 插入元素到红黑树中的方法。
final TreeNode<K, V> putTreeVal(HashMap<K, V> map, Node<K, V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
// 标记是否找到这个key的节点
boolean searched = false;
// 找到树的根节点
TreeNode<K, V> root = (parent != null) ? root() : this;
// 从树的根节点开始遍历
for (TreeNode<K, V> p = root; ; ) {
// dir=direction,标记是在左边还是右边
// ph=p.hash,当前节点的hash值
int dir, ph;
// pk=p.key,当前节点的key值
K pk;
if ((ph = p.hash) > h) {
// 当前hash比目标hash大,说明在左边
dir = -1;
}
else if (ph < h)
// 当前hash比目标hash小,说明在右边
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
// 两者hash相同且key相等,说明找到了节点,直接返回该节点
// 回到putVal()中判断是否需要修改其value值
return p;
else if ((kc == null &&
// 如果k是Comparable的子类则返回其真实的类,否则返回null
(kc = comparableClassFor(k)) == null) ||
// 如果k和pk不是同样的类型则返回0,否则返回两者比较的结果
(dir = compareComparables(kc, k, pk)) == 0) {
// 这个条件表示两者hash相同但是其中一个不是Comparable类型或者两者类型不同
// 比如key是Object类型,这时可以传String也可以传Integer,两者hash值可能相同
// 在红黑树中把同样hash值的元素存储在同一颗子树,这里相当于找到了这颗子树的顶点
// 从这个顶点分别遍历其左右子树去寻找有没有跟待插入的key相同的元素
if (!searched) {
TreeNode<K, V> q, ch;
searched = true;
// 遍历左右子树找到了直接返回
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
// 如果两者类型相同,再根据它们的内存地址计算hash值进行比较
dir = tieBreakOrder(k, pk);
}
/*
* 插入元素的逻辑
*/
TreeNode<K, V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// 如果最后确实没找到对应key的元素,则新建一个节点
Node<K, V> xpn = xp.next;
TreeNode<K, V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K, V>) xpn).prev = x;
// 插入树节点后平衡
// 把root节点移动到链表的第一个节点
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
(1)寻找根节点;
(2)从根节点开始查找;
(3)比较 hash 值及 key 值,如果都相同,直接返回,在 putVal() 方法中决定是否要替换 value 值;
(4)根据 hash 值及 key 值确定在树的左子树还是右子树查找,找到了直接返回;
(5)如果最后没有找到则在树的相应位置插入元素,并做平衡;
treeifyBin()
- 如果插入元素后链表的长度大于等于 8 则判断是否需要树化。
final void treeifyBin(Node<K, V>[] tab, int hash) {
int n, index;
Node<K, V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// 如果桶数量小于64,直接扩容而不用树化
// 因为扩容之后,链表会分化成两个链表,达到减少元素的作用
// 当然也不一定,比如容量为4,里面存的全是除以4余数等于3的元素
// 这样即使扩容也无法减少链表的长度
resize();
// 将当前桶位进行树化
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K, V> hd = null, tl = null;
// 把所有节点换成树节点
do {
TreeNode<K, V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
// 如果进入过上面的循环,则从头节点开始树化
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
TreeNode.treeify()
- 真正树化的方法。
final void treeify(Node<K, V>[] tab) {
TreeNode<K, V> root = null;
for (TreeNode<K, V> x = this, next; x != null; x = next) {
next = (TreeNode<K, V>) x.next;
x.left = x.right = null;
// 第一个元素作为根节点且为黑节点,其它元素依次插入到树中再做平衡
if (root == null) {
x.parent = null;
x.red = false;
root = x;
} else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
// 从根节点查找元素插入的位置
for (TreeNode<K, V> p = root; ; ) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
// 如果最后没找到元素,则插入
TreeNode<K, V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
// 插入后平衡,默认插入的是红节点,在balanceInsertion()方法里
root = balanceInsertion(root, x);
break;
}
}
}
}
// 把根节点移动到链表的头节点,因为经过平衡之后原来的第一个元素不一定是根节点了
moveRootToFront(tab, root);
}
(1)从链表的第一个元素开始遍历;
(2)将第一个元素作为根节点;
(3)其它元素依次插入到红黑树中,再做平衡;
(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
创建出新的哈希表 - 扩容操作
- ① 该桶中只有一个元素,直接通过路由寻址算法得到新下标 插入到新位置
- ② 当前节点已树化,由树化操作寻址插入
- ③ 当前节点为链表结构,分离链表(低位、高位),根据位运算判断当前节点应存至低位/高位
- 存放操作
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; // 没有数据直接返回空
}
(1)计算 key 的 hash 值;
(2)找到 key 所在的桶及其第一个元素;
(3)如果第一个元素的 key 等于待查找的 key,直接返回;
(4)如果第一个元素是树节点就按树的方式来查找,否则按链表方式查找;
TreeNode.getTreeNode()
- 红黑树的查找操作。
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
return null;
}
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); // 后续LinkedHashMap源码解析的文章会分析这个钩子方法
return node; // 返回结果
}
}
return null; // 如果查询不到 则返回null
}
(1)先查找元素所在的节点;
(2)如果找到的节点是树节点,则按树的移除节点处理;
(3)如果找到的节点是桶中的第一个节点,则把第二个节点移到第一的位置;
(4)否则按链表删除节点处理;
(5)修改 size,调用移除节点后置处理等;
TreeNode.removeTreeNode()
- 红黑树的删除操作。
final void removeTreeNode(HashMap<K, V> map, Node<K, V>[] tab,
boolean movable) {
int n;
// 如果桶的数量为0直接返回
if (tab == null || (n = tab.length) == 0)
return;
// 节点在桶中的索引
int index = (n - 1) & hash;
// 第一个节点,根节点,根左子节点
TreeNode<K, V> first = (TreeNode<K, V>) tab[index], root = first, rl;
// 后继节点,前置节点
TreeNode<K, V> succ = (TreeNode<K, V>) next, pred = prev;
if (pred == null)
// 如果前置节点为空,说明当前节点是根节点,则把后继节点赋值到第一个节点的位置,相当于删除了当前节点
tab[index] = first = succ;
else
// 否则把前置节点的下个节点设置为当前节点的后继节点,相当于删除了当前节点
pred.next = succ;
// 如果后继节点不为空,则让后继节点的前置节点指向当前节点的前置节点,相当于删除了当前节点
if (succ != null)
succ.prev = pred;
// 如果第一个节点为空,说明没有后继节点了,直接返回
if (first == null)
return;
// 如果根节点的父节点不为空,则重新查找父节点
if (root.parent != null)
root = root.root();
// 如果根节点为空,则需要反树化(将树转化为链表)
// 如果需要移动节点且树的高度比较小,则需要反树化
if (root == null
|| (movable
&& (root.right == null
|| (rl = root.left) == null
|| rl.left == null))) {
tab[index] = first.untreeify(map); // too small
return;
}
// ------------分割线,以上都是删除链表中的节点,下面才是直接删除红黑树的节点(因为TreeNode本身即是链表节点又是树节点)
// 删除红黑树节点的大致过程是寻找右子树中最小的节点放到删除节点的位置,然后做平衡,此处不过多注释
TreeNode<K, V> p = this, pl = left, pr = right, replacement;
if (pl != null && pr != null) {
TreeNode<K, V> s = pr, sl;
while ((sl = s.left) != null) // find successor
s = sl;
boolean c = s.red;
s.red = p.red;
p.red = c; // swap colors
TreeNode<K, V> sr = s.right;
TreeNode<K, V> pp = p.parent;
if (s == pr) { // p was s's direct parent
p.parent = s;
s.right = p;
} else {
TreeNode<K, V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
p.left = null;
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s;
if ((s.parent = pp) == null)
root = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
if (sr != null)
replacement = sr;
else
replacement = p;
} else if (pl != null)
replacement = pl;
else if (pr != null)
replacement = pr;
else
replacement = p;
if (replacement != p) {
TreeNode<K, V> pp = replacement.parent = p.parent;
if (pp == null)
root = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}
TreeNode<K, V> r = p.red ? root : balanceDeletion(root, replacement);
if (replacement == p) { // detach
TreeNode<K, V> pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
if (movable)
moveRootToFront(tab, r);
}
(1)TreeNode 本身既是链表节点也是红黑树节点;
(2)先删除链表节点;
(3)再删除红黑树节点并做平衡;
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;
}
总结
(1)HashMap 是一种散列表,采用**(数组 + 链表 + 红黑树)**的存储结构;
(2)HashMap 的默认初始容量为 16(1 << 4),默认装载因子为 0.75f,容量总是 2 的 n 次方;
(3)HashMap 扩容时每次容量变为原来的两倍;
(4)当桶的数量小于 64 时不会进行树化,只会扩容;
(5)当桶的数量大于 64 且单个桶中元素的数量大于 8 时,进行树化;
(6)当单个桶中元素数量小于 6 时,进行反树化;
(7)HashMap 是非线程安全的容器;
(8)HashMap 查找添加元素的时间复杂度都为 O ( 1 ) O(1) O(1);
参考
- 视频参考
- b站_小刘讲源码HashMap
- 文章参考
- shstart7_面试必问之HashMap源码解析
- 彤哥读源码_死磕 java集合之HashMap源码分析