JDK-In-Action-HashMap
HashMap
非线程安全的散列表实现,采用链表法解决Hash冲突.
优化点:
- hash函数,避免key的hashcode()设计糟糕导致hash冲突严重
- 冲突链表转红黑树,线性查找改进为对数查找
- 2倍扩容, 简单.(相对于素数桶大小而言)
源码导读
初始构造容量大小
int tableSizeFor(int cap)
返回最接近cap的2的幂大小,例如 1,2,4,8,16...
指定初始容量或者从集合数据构造,都调用tableSizeFor()
方法来确定threshold
值.
元素大小阀值
int threshold;
capacity * load factor , 元素大小超过该阀值需要扩容
负载因子
final float loadFactor;
容器会在size > threshold=(cap*loadFactor) 时扩容2倍
默认负载因子为 0.75f
底层数据结构
transient Node<K,V>[] table;
线性表, 通过 hash(key) & (cap-1)
来求索引位置
扩容步骤
final Node<K,V>[] resize()
关键设计点:
因为扩容是2倍扩容newCap=oldCap*2,所以新容量始终是2的幂.
可以观察到,一个Node在new数组中的索引值,要么不变,要么需要加oldCap,解释如下:
oldCap=16,16-1=15, 0 1 1 1 1
newCap=32,32-1=31, 1 1 1 1 1
-------------------------------------&
A_Node.hash 0 0 1 0 1
B_Node.hash 1 0 1 0 1
可以观察到仅当Node.hash值的高位是1,新的索引值才会发生变化,也就是B_Node.
B_Node 在old中的索引是 0101 , 在new中的索引为 1101=0101+oldCap
那么如何判断Node在新的table中索引是否需要加oldCap?
计算 (e.hash & oldCap) == 0 ? oldCap的低位为0,所以仅当e.hash的高位与oldCap二进制的高为均为1时结果才不为0
true=>A类节点
false=>B类节点
- 对于没有hash冲突的key,重新计算索引位置
newTab[e.hash & (newCap - 1)] = e;
- 对于红黑树的节点,按A类节点和B类节点进行分割(两类节点在新table中的索引不一样),如果新树太小则需要转换成链表形式
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
- 这一段代码用于处理有hash冲突的链表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {//扩容后索引位置不变的key
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {//扩容后索引位置需要+oldCap的key
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;//新索引需要+oldCap
}
红黑树Hash冲突优化
//TODO
Hash函数-Hash扰动优化
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
因为HashMap使用的是长度为2的幂的散列表(扩容更容易,另一种方案是素数查找), 会遇到在低位上没有差异的hashcode的冲突. 对给定key的hashCode应用一个附加的散列函数,以防止质量较差的散列函数导致严重的hash冲突.
另见: https://stackoverflow.com/questions/15437345/java-a-prime-number-or-a-power-of-two-as-hashmap-size
Hash冲突优化-双向链表转红黑树
插入数据时,如果出现冲突,将扫描冲突索引的链表元素个数binCount , 如果 binCount >= TREEIFY_THRESHOLD - 1
则进行转化:
final void treeifyBin(Node<K,V>[] tab, int hash)
clear操作
size=0;
将底层 table 数组的每一个元素引用赋值null
API Example
元素操作之 Put
Map<Integer, Object> map = new HashMap<>();
//添加单个映射,会覆盖已存在的值
Object old = map.put(1, 0);
assertEquals(old, null);
//返回被覆盖的值
old = map.put(1, 0);
assertEquals(old, 0);
Map<Integer, Object> src = new HashMap<>();
src.put(1, 1);
src.put(99, 1);
//批量添加映射,会覆盖已存在的值
map.putAll(src);
//添加映射,key不存在才添加
old = map.putIfAbsent(1, 2);
assertEquals(old, 1);
old = map.putIfAbsent(2, 100);
assertEquals(old, null);
元素操作之 Remove
Map<Integer, Object> map = new HashMap<>();
map.put(1, 1);
map.put(2, 2);
map.put(3, 3);
map.put(4, 4);
//移除指定key
Object old = map.remove(0);
assertEquals(old, null);
old = map.remove(1);
assertEquals(old, 1);
//移除指定key,且指定value
boolean remove = map.remove(2, 3);
assertEquals(remove, false);
remove = map.remove(2, 2);
assertEquals(remove, true);
元素操作之 Replace
Map<Integer, Object> map = new HashMap<>();
map.put(1, 1);
map.put(2, 2);
map.replace(1, 10);
map.replace(2, 2, 200);
assertEquals(map, "{1=10, 2=200}");
元素操作之查找
Map<Integer, Object> map = new HashMap<>();
map.put(1, 1);
map.put(2, 2);
//键值访问
Object value = map.get(1);
assertEquals(value, 1);
//键是否包含
boolean containsKey = map.containsKey(1);
assertEquals(containsKey, true);
//值是否包含
boolean containsValue = map.containsValue(2);
assertEquals(containsKey, true);
构造函数之初始化容量和负载因子参数
HashMap<Integer, Object> hashMap = new HashMap<>(128, 0.75f);
println("容器仅在首次使用时才分配内存");
hashMap.put(1, 1);
assertEquals(hashMap, "{1=1}");
容器仅在首次使用时才分配内存
构造函数之拷贝其他 Map
Map<Integer, Object> src = new HashMap<>();
src.put(1, 1);
HashMap<Integer, Object> hashMap = new HashMap<>(src);
hashMap.put(2, 1);
assertEquals(hashMap, "{1=1, 2=1}");
构造函数之空 Map
HashMap<Integer, Object> hashMap = new HashMap<>();
assertEquals(hashMap, "{}");
迭代操作之 Entry Set
Map<Integer, Object> map = new HashMap<>();
map.put(1, 1);
map.put(2, 2);
//key-value的集合,无序,对entrySet的操作会反应到源map
Set<Map.Entry<Integer, Object>> entrySet = map.entrySet();
Iterator<Map.Entry<Integer, Object>> iterator = entrySet.iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, Object> next = iterator.next();
println(next);
}
println("----OR----");
for (Map.Entry entry : map.entrySet()) {
println(entry);
}
1=1
2=2
----OR----
1=1
2=2
迭代操作之 Foreach
Map<Integer, Object> map = new HashMap<>();
map.put(1, 1);
map.put(2, 2);
map.forEach((k, v) -> printTable(k, v));
1 | 1 |
2 | 2 |
迭代操作之 Key Set
Map<Integer, Object> map = new HashMap<>();
map.put(1, 1);
map.put(2, 2);
//key的集合,无序,对keySet的操作会反应到源map
Set<Integer> keySet = map.keySet();
assertEquals(keySet, "[1, 2]");
//键移除会导致映射移除
keySet.remove(1);
Object value = map.get(1);
assertEquals(value, null);
// keySet.add(3);//不支持的操作
keySet.clear();
assertEquals(map.isEmpty(), true);
引用
- 源码下载
- JDK8
java.util.HashMap
Source Code - Java中HashMap的大小为什么是2的幂而不是一个素数