Java集合 HashMap小结
①存储结构
- 数组
- 链表
- 红黑树
②冲突解决
拉链法
常见冲突解决方式:
- 开放地址法:线性探测、平方探查、双散列函数探查
- 拉链法
- 再哈希法
- 建立公共溢出区
③hash
^运算符:对应位相同,值为0,对应位不同,值为1
即:0^0=0, 0^1=1, 1^0=1
常用:x^0=x
说明:key的hashCode,
static final int hash(Object key) {
int h;
// 高16位:h高16位 ^ 0 = h高16位
// 低16位:h低16位 ^ h高16位
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
④定位桶
- 计算hash:hash(key)
- 定位桶:hash & (n-1)
④定位节点
下面比较:&&
- 当前节点.hash == hash
- 当前节点.key == key(或equals)
- 定位桶,前提:桶非空
- 比较桶.first
- 比较桶.剩余节点
final HashMap.Node<K,V> getNode(int hash, Object key) {
HashMap.Node<K,V>[] tab = this.table;
int n = tab.length;
// 桶存在
if (tab != null && n > 0) {
// 定位key对应桶
int keyIndex = (n - 1) & hash;
HashMap.Node<K,V> first = tab[keyIndex];
// 桶.第一个节点
if (first != null) {
int curHash = first.hash;
int curKey = first.key;
if (curHash == hash && (curKey == key || (key != null && key.equals(curKey)))) {
return first;
}
// 桶.剩余节点
HashMap.Node<K,V> e = first.next;
if (e != null) {
// 红黑树节点处理
if (first instanceof TreeNode) {
return ((HashMap.TreeNode<K,V>)first).getTreeNode(hash, key);
}
// 链表节点处理
do {
curHash = e.hash;
curKey = e.key;
if (curHash == hash && (curKey == key || (key != null && key.equals(curKey)))) {
return e;
}
} while ((e = e.next) != null);
}
}
}
return null;
}
⑤扩容机制
扩容大小:2倍。具体流程略,有兴趣自行debug或code
概念 | 变量 | 描述 |
---|---|---|
容量 | capacity | 2^n |
负载因子 | loadFactor | 0.75f |
阈值 | threshold | capacity * loadFactor |
大小 | size | 节点个数 |
扩容条件:
- 未初始化,进行put,例如:默认构造,put
- 节点个数达到阈值上限,即:size > threshold
- 链表树化,不满足树化条件则扩容:桶未初始化 || 桶大小 < 64,即
tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY
- computeIfAbsent compute merge等方法,略
说明:HashMap默认构造器,没有指定threshold,采用lazy load,第一次put才初始化桶数组
⑥树化和链表化
树化条件:
- 对应桶节点个数大于等于8
- 哈希容量不小于64
⑦快速失败机制 fail-fast
避免并发修改场景。基本原理是通过modCount的类CAS(读然后验证)操作
⑧JDK 7的死循环
后果:CPU 100%,桶节点成环,get始终不结束
JDK 7的resize:头插法(桶链表resize后会逆序)
并发resize导致死循环场景:
桶链表:[first]: a->b->c。插入第一个节点d,发生rehash
1、线程1时间片内:e指向a,e.next指向b
2、线程2时间片内:形成resize桶 c->b->c
3、线程1时间片内:成环。a->b->a->b…
其他
key要求不可变,原因是equals和hashCode,比较。