HashMap 原理
成员变量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 数组默认长度
static final int MAXIMUM_CAPACITY = 1 << 30; // 数组长度最大值
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认加载因子,数组长度达到 75% 就扩容
// TREEIFY_THRESHOLD、MIN_TREEIFY_CAPACITY 同时达到才会转红黑树
static final int TREEIFY_THRESHOLD = 8; // 添加元素,数组里的链表 >= 8 转为红黑树
static final int MIN_TREEIFY_CAPACITY = 64; // 添加元素,数组长度 >= 64 时,转为红黑树
static final int UNTREEIFY_THRESHOLD = 6; // 删除元素,数组里的链表默认 <= 6 时,如果是红黑树转为链表
transient Node<K,V>[] table; // 存储数据的数组
transient Set<Map.Entry<K,V>> entrySet; // 数据的集合
transient int size; // 元素个数
transient int modCount; // 修改次数
int threshold; // 扩容临界值,第一次 put 时初始化为 12,后面每次扩容维护一次
final float loadFactor; // 自定义的加载因子(如果创建实例时,指定了加载因子,默认的就不生效)
put 过程
假设是第一次 put ,只是看 put 过程,先不考虑 hash 碰撞、红黑树那些
// 入口方法
public V put(K key, V value) {
// 先得到 key 的 hash 值
return putVal(hash(key), key, value, false, true);
}
// putVal 过程
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) // 第一次进来 table 数组是空,
n = (tab = resize()).length; // resize() 创建了一个长度是 16 的数组,扩容阈值是 12(虽然里面比较复杂,但是只看第一次添加,代码很简单就不贴了)
// 此时数组下标是否为空,如果为空,床架一个节点,放入数组此下标
if ((p = tab[i = (n - 1) & hash]) == null) // n 数组长度,再跟 key 的 hash 做与运算,这是在算 key 的下标
tab[i] = newNode(hash, key, value, null);
else {
... 因为看第一次 put,这个 else 是不会走的
}
++modCount; // 修改次数+1
if (++size > threshold) // 维护 size,并判断是否需要扩容,达到扩容阈值没
resize(); // 第一次添加,不会扩容
afterNodeInsertion(evict); // LinkedHashMap 实现,跟 ArrayList 没关系
return null;
}
hash 冲突
如果第二次添加,先假存在 hash 冲突,但是值不一样,因为如果值一样直接就覆盖完事儿了
// 还是 putVal 方法
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;
if ((p = tab[i = (n - 1) & hash]) == null) // 哈希冲突,这时 key 算出来的下标在数组中就有值了,不会进这个判断
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 的 hash 值,再用 equals 判断 value
e = p; // 如果相等,直接覆盖
else if (p instanceof TreeNode) // 已经存在的元素如果是树节点(已经是红黑树了)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 添加到树里里面去
else { // 如果不是树节点,那就是链表
for (int binCount = 0; ; ++binCount) {
// p 是已存在的节点,p 的下一个节点如果为空
if ((e = p.next) == null) { // 尾插法,8 之前是头插法(七上八下)
p.next = newNode(hash, key, value, null); // 把这次 put 的数据封装成节点,并作为 p 的下一个节点(HashMap 是单向链表)
// 8-1=7,下标是7,第八个元素,如果已经遍历到这里,那就说明可能要转成红黑色了(如果数组长度也达到64才会真正转)
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash); // 这个方法里面会判断数组长度是否达到 64,如果达到了,数据结构就转为红黑树
break; // 到这里就结束了,如果不用转红黑树,单向链表已经维护了,如果要转红黑树,也转了
}
// 如果 p 的下一个节点不为空才会这里,这里判断下一个节点,是否需要覆盖
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break; // 如果需要覆盖,直接 break?因为这个循环的第一个判断就把该节点指给了 e,所以 e 不为空,跳出后就执行覆盖逻辑了
p = e; // 如果不需要覆盖,再把 e 指给 p 这个变量,然后进入链表的下一个节点遍历
}
}
// 如果 e 不为空,就是 hashCode 和 equals 都相同,要覆盖值
if (e != null) { // existing mapping for key
V oldValue = e.value; // e 原来的数据
// 原来的数据为空或onlyIfAbsent为false,就进这个判断,判断也很简单,就是把新的 value 设置为节点的值
if (!onlyIfAbsent || oldValue == null) // onlyIfAbsent 传参进来的写死的是 false
e.value = value; // put 带过来的 value 设置到节点的 value,完成值的覆盖
afterNodeAccess(e); // LinkedHashMap 实现,跟 ArrayList 没关系
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict); // LinkedHashMap 实现,跟 ArrayList 没关系
return null;
}
转红黑树
细节后面再揪,现在先点到为止吧。树的变种很多,二叉,多叉,二叉又分为完全、完满、平衡、AVL、红黑,目前还不具备这块能力 _
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
resize(); // 扩容,并不会转红黑树
else if ((e = tab[index = (n - 1) & hash]) != null) { // 当前节点取出来赋值给 e,e 就是当前节点,这个链表的节点要维护成红黑树的节点
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null); // 这里把 e 转成红黑树节点
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);
}
}
扩容
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) {
// 如果数组长度已经达到 MAXIMUM_CAPACITY(最大值)
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab; // 达到了最大值,不会再扩容,直接把原来的数组返回去
}
// 如果老的长度达到 16,新长度就是老长度的 2 倍,oldCap << 1 就是 * 2 的意思
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
// 如果新的长度没达到 MAXIMUM_CAPACITY,扩容阈值也扩大到两倍
// 如果新长度没达到最大值,不设置扩容阈值,下一次再扩容的时候走上面的判断,赋值为 Integer.MAX_VALUE
newThr = oldThr << 1;
}
// 有参构造创建的 HashMap,长度设置为根据参数计算出来的扩容阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // 无参构造创建的 HashMap,长度设置为 16,扩容阈值设置为 16*0.75 = 12
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) { // 带了参数创建的 HashMap 前面只设置了长度,这里设置下下一次的扩容阈值
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; // 新数组赋值给 table
if (oldTab != null) { // 如果老数据有数据,要把老数组的数据迁移到新数组
for (int j = 0; j < oldCap; ++j) {
// 因为长度变化了,原来数据对应的下标可能发生变化,会根据数组长度重新计算下标
... 具体代码就不贴了
}
}
return newTab;
}
1.7 死链
出现在并发扩容(两个线程调用 transfer 方法,当一个已经完成扩容,另一个线程正在扩容中),jdk1.7 采用的是头插法,在极端情况会出现死链
- 比如数组下标为 1 的元素是个链表,存放的数据是 1 --> 35 --> 16 --> null
- 线程 A 线程、B 同时扩容
- 线程 A 拿到了 table[1] 的数据,先拿到 1,存放在线程 A 的局部变量,然后拿下一个元素 35 也放在局部变量...(假设线程 A 就刚好走在这里)
- 线程 B 先完成扩容,扩容后 table[1] 放的是 35 --> 1 --> null(16 假设放到另一个数组下标了,头插法所以 35 在 1 前面)
- 线程 A 已经拿到数据为:1 --> 35;继续拿下一个,此时线程 B 已经把 35 的下一个元素设置成了1,此时就产生死链了(下一个元素是自己的上一个元素)
分类:
JAVA - JDK
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具