1. HashMap 底层数据结构
HashMap 于 JDK1.2 中首次出现,JDK1.7 及以前底层数据结构为数组+链表「图 1」,JDK1.8 及以后更新为数组+链表+红黑树「图 2」
图 1
图 2
1. 将元素存储在数组中
2. 随着元素的增加,不再利于增删操作,因此加入了链表来提升效率
数组中每个元素皆关联一条链表
3. 随着元素的继续增加,不再利于查找操作,因此将链表改进为红黑树
值得注意的是,只有在链表长度大于 8 且 数组长度大于 64 时才会将链表替换为红黑树
2. 新增操作 - 流程
1)图解【×】
图 3
2)文字叙述
第一步,调用 put 方法传入键值对
第二步,使用 Hash 算法计算 Hash 值
第三步,判断是否发生了哈希碰撞
第四步,若未发生,则将元素存储在数组中;反之,则判断当前底层数据结构是链表还是红黑树
第五步,若是红黑树,则将元素存储在红黑树中;反之,则将元素存储在链表中
第六步,判断链表长度是否大于 8,若是,则将链表替换为红黑树
3)代码分析
① put 方法
调用 put 方法传入键值对
public V put(K key, V value) {
return this.putVal(hash(key), key, value, false, true);
}
② hash 方法
使用 Hash 算法计算 Hash 值
static final int hash(Object key) {
int h;
return key == null ? 0 : (h = key.hashCode()) ^ h >>> 16;
}
③ putVal 方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
HashMap.Node[] tab;
int n;
if ((tab = this.table) == null || (n = tab.length) == 0) {
n = (tab = this.resize()).length;
}
Object p;
int i;
if ((p = tab[i = n - 1 & hash]) == null) {
tab[i] = this.newNode(hash, key, value, (HashMap.Node)null);
} else {
Object e;
Object k;
if (((HashMap.Node)p).hash == hash && ((k = ((HashMap.Node)p).key) == key || key != null && key.equals(k))) {
e = p;
} else if (p instanceof HashMap.TreeNode) {
e = ((HashMap.TreeNode)p).putTreeVal(this, tab, hash, key, value);
} else {
int binCount = 0;
while(true) {
if ((e = ((HashMap.Node)p).next) == null) {
((HashMap.Node)p).next = this.newNode(hash, key, value, (HashMap.Node)null);
if (binCount >= 7) {
this.treeifyBin(tab, hash);
}
break;
}
if (((HashMap.Node)e).hash == hash && ((k = ((HashMap.Node)e).key) == key || key != null && key.equals(k))) {
break;
}
p = e;
++binCount;
}
}
if (e != null) {
V oldValue = ((HashMap.Node)e).value;
if (!onlyIfAbsent || oldValue == null) {
((HashMap.Node)e).value = value;
}
this.afterNodeAccess((HashMap.Node)e);
return oldValue;
}
}
++this.modCount;
if (++this.size > this.threshold) {
this.resize();
}
this.afterNodeInsertion(evict);
return null;
}
A. 第 1 部分
判断数组的地址或内容是否为空,若为空,则创建一个新的数组
tab - 数组
判断数组的地址是否为空:(tab = this.table) == null
判断数组的内容是否为空: (n = tab.length) == 0
if ((tab = this.table) == null || (n = tab.length) == 0) {
n = (tab = this.resize()).length;
}
B. 第 2 部分
判断当前元素插入节点是否有值,若为空,则将元素存储在数组中
p - 插入节点
由 n - 1 & hash 计算当前元素存储在数组中的位置(i)
判断是否发生了地址冲突:(p = tab[i]) == null
if ((p = tab[i = n - 1 & hash]) == null) {
tab[i] = this.newNode(hash, key, value, (HashMap.Node)null);
}
C. 第 3 部分
发生了地址冲突
else {
Object e;
Object k;
if (((HashMap.Node)p).hash == hash && ((k = ((HashMap.Node)p).key) == key || key != null && key.equals(k))) {
e = p;
} else if (p instanceof HashMap.TreeNode) {
e = ((HashMap.TreeNode)p).putTreeVal(this, tab, hash, key, value);
} else {
int binCount = 0;
while(true) {
if ((e = ((HashMap.Node)p).next) == null) {
((HashMap.Node)p).next = this.newNode(hash, key, value, (HashMap.Node)null);
if (binCount >= 7) {
this.treeifyBin(tab, hash);
}
break;
}
if (((HashMap.Node)e).hash == hash && ((k = ((HashMap.Node)e).key) == key || key != null && key.equals(k))) {
break;
}
p = e;
++binCount;
}
}
if (e != null) {
V oldValue = ((HashMap.Node)e).value;
if (!onlyIfAbsent || oldValue == null) {
((HashMap.Node)e).value = value;
}
this.afterNodeAccess((HashMap.Node)e);
return oldValue;
}
}
a. 第 3 部分第 1 小节
判断 tab[i] 的键与当前元素的键是否相同,若相同,则将节点 p 赋值给 节点 e
节点 p 等同于 tab[i]
if (((HashMap.Node)p).hash == hash && ((k = ((HashMap.Node)p).key) == key || key != null && key.equals(k))) {
e = p;
}
b. 第 3 部分第 2 小节
判断当前底层数据结构是否为红黑树,若是,则将元素存储在红黑树中
instanceof:判断其左边对象是否为其右边类的实例,返回 boolean 类型的数据
else if (p instanceof HashMap.TreeNode) {
e = ((HashMap.TreeNode)p).putTreeVal(this, tab, hash, key, value);
}
c. 第 3 部分第 3 小结
从节点 p 的后继结点开始遍历链表,若遍历到链表表尾仍未发生哈希碰撞或键相同的情况,则将元素存储在链表尾结点中,而后判断链表长度是否大于 8,若大于 8,将链表替换为红黑树;若遍历链表时发生了哈希碰撞或键相同的情况,则结束循环。
哈希碰撞:p != null 和 p.hash == hash
else {
int binCount = 0;
while(true) {
if ((e = ((HashMap.Node)p).next) == null) {
((HashMap.Node)p).next = this.newNode(hash, key, value, (HashMap.Node)null);
if (binCount >= 7) {
this.treeifyBin(tab, hash);
}
break;
}
if (((HashMap.Node)e).hash == hash && ((k = ((HashMap.Node)e).key) == key || key != null && key.equals(k))) {
break;
}
p = e;
++binCount;
}
}
D. 第 4 部分
【续】第 3 部分第 1 小结或第 3 部分第 3 小结
判断当前元素的插入节点 e 是否有值,若有值,则表明节点 e 的键与当前元素的键相同,将节点 e 的值替换为当前元素的值
if (e != null) {
V oldValue = ((HashMap.Node)e).value;
if (!onlyIfAbsent || oldValue == null) {
((HashMap.Node)e).value = value;
}
this.afterNodeAccess((HashMap.Node)e);
return oldValue;
}
F. 第 5 部分
判断是否需要扩容
++this.modCount;
if (++this.size > this.threshold) {
this.resize();
}
3. 扩容操作 - 流程【未掌握】
图解
图 4
4. 解决地址冲突问题 - 链地址法
因为由 n - 1 & hash 计算当前元素存储在数组中的位置(i),所以地址冲突的前提是 hash 相同
底层数据结构:数组 + 链表
链地址法:取数据元素的关键字 key,计算其散列函数值 i(地址 - 存储位置)。若 tab[i] 关联的链表为空,则将该数据元素插入该链表;反之,则将该数据元素插入该链表头节点的后继结点或尾结点中
5. 非线程安全