主要参考文章:
1、Java7/8中的HashMap和ConcurrentHashMap源码分析,看了都说好
2、源码分析之HashMap的红黑树实现
3、Java 集合深入理解(17):HashMap 在 JDK 1.8 后新增的红黑树结构
4、2.1.4. Java集合——HashMap
对于 HashMap,主要的几个问题。
1、解决哈希冲突的方法
2、为什么数组的长度是 2n
3、扩容机制的实现
这里只记录关键的源码相关的内容。基于 Java8
首先回答 问题 1
HashMap 中解决哈希冲突的方法就是使用 数组+链表 的结构(Java8 中的红黑树暂时忽略),数组的每个元素存储的是链表的头节点,而链表是由同一类 key-value 包装成节点组成的。
同一类的概念是指利用 key 的 hash 值映射为数组下标 index 时,多个不同的 key 映射的结果都为同一 index,此时,就把这多个 key 对应的节点组成一个链表,存放于数组[index]
中。
这样做的根本目的,也是为了加快查找速度。
而结合上文, 问题 2 也可以得到解答。
数组的长度是 2n,就是为了结合位运算提高 key 的映射效率,因为当数组长度 length 为 2 的 n 次方时,对于 key 的 hash 值,有 hash%length == hash&(length-1)
。
当 length 是 2n 时,此时(length - 1) 的二进制全是1, hash & (length -1)
相当于取 hash值的低 n 位( hash 值低 n 位组成的值 x 肯定是小于 2n 的,而此时 x % 2n 则就是 x 本身), 结果和 hash%length
一样的。
然后先看下几个关键的方法:
capacity:当前数组容量,始终保持2^n,可以扩容,扩容后的大小为当前的2倍,默认为16。
loadFactor:负载因子,默认为 0.75。
threshold:扩容的阈值,等于 capatity * loadFactor。
首先是其中一个构造方法:
public HashMap(int initialCapacity, float loadFactor) {
...
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
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;
}
这个构造方法是可以指定初始化数组的大小的,但是实际上并不是直接用该 initialCapacity
值,而是使用与 initialCapacity
最接近的且是 2n 的值。且这里不会立即初始化数组。
然后是 put() 方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
// 如果数组为空或者长度为 0,则先根据 capacity 初始化对应大小的数组
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (n - 1) & hash 即将对应的 hash 值映射到数组对应的下标 i
// 如果 tab[i] 为 null,则表示该 key-value 还不存在于 map 中,是新的组合,
// 则直接存放在 tab[i] 即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 否则,就表示下标 i 处已经有了多个节点,因此需要对这些节点进行比较
// 就会有几种情况:
// 1、这些节点中的某一节点的 key 值与当前 key 一致,
// 则需要用当前 value 覆盖节点的原 value 值
// 2、这些节点中不存在与当前 key 值相等的节点,
// 因此当前 key-value 同样也是新的组合,需要添加到 tab[i] 的节点中
else {
Node<K,V> e;
K k;
// 如果 tab[i] 的首个节点 p 的 key 即与当前 key 相等,则该节点 p 就是目标节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果 tab[i] 的首个节点 p 不是目标节点,且 tab[i] 处的数个节点组成的是红黑树,
// 则针对该红黑树进行遍历插入
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 走到这里,就表示 tab[i] 处数个节点组成的还是单链表,
// 且需要遍历这个单链表
for (int binCount = 0; ; ++binCount) {
// 如果遍历完之后,还是没有找到目标节点的 key 与 当前 key 值相等的,
// 则把当前 key-value 实例成一个新的节点插入到链表末尾
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 在新加了节点之后,如果该链表的节点数达到了某个值,则将链表转换为红黑树
// TREEIFY_THRESHOLD == 8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果在遍历的过程中找到了目标节点的 key 与 当前 key 值相等的,
// 则此时即赋值为了 p
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// e 不为 null,则表示还没有遍历完就找到了目标节点 p,
// 则将当前的 value 覆盖原有的 value 值即可
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果总的节点数大于了 threshold,则需要将数组扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
// 用于将单链表转换为红黑树
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 如果数组大小 < MIN_TREEIFY_CAPACITY(64) 则不转换,而是将数组进行扩容
// 因为扩容之后就减少该单链表的节点数,提高遍历查询的效率(红黑树也是为了提高查询效率)
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 否则,就真的将单链表转换为红黑树
else if ((e = tab[index = (n - 1) & hash]) != null) {
// 将 tab[index] 处的节点(Node 类型的)转换为 TreeNode 类型的
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);
// 将转换成的 TreeNode 节点们构成红黑树
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
put()
方法的实际作用就将 key-value 组合存放到 map 中,但是此时 key 值可能已经存在于 map 中了,则只要更新 value 即可,否则就表示原来不存在,需要新增到 map 中。
在新增了之后,当新增节点的链表上的元素个数 >= 8 时且数组的长度为 64 时,则会将该链表转换为红黑树,以提高遍历查询的效率。而如果该链表上的元素个数 >= 8 时且数组的长度小于 64,则只会将数组扩容,而不触发转换红黑树的操作。
另外,当 map 存储的元素个数大于 threshold 时,也会触发扩容。
然后是 get(key) 方法:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab;
Node<K,V> first, e;
int n;
K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
get() 方法则比较简单直观,先根据 key 的 hash 值得到对应的映射下标 index,然后在 tab[index] 对应的数个元素中去查找与 key 相等的节点即可。只不过找的时候分为在链表中找或者在红黑树中找两种情况。
以及扩容方法 resize()
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 (1<<30)
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;// 则将扩容阀值设置为 Integer.MAX_VALUE
// 且直接返回原数组,不再进行扩容
return oldTab;
}
// 否则新的数组大小为原大小的 2 倍且小于 MAXIMUM_CAPACITY
// 且原数组大小 >= DEFAULT_INITIAL_CAPACITY(16)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// oldCap <= 0 且 oldThr > 0
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// oldCap <= 0 且 oldThr <= 0
else { // zero initial threshold signifies using defau
newCap = DEFAULT_INITIAL_CAPACITY;
// newThr = 0.75 * 16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPAC
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 遍历原数组,将每个数组中的元素节点重新调整到新数组对应的位置
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 如果 oldTab[j] 只有一个元素,
// 则直接置于 newTab[e.hash & (newCap - 1)] 处
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 如果 oldTab[j] 处是红黑树
// 并且元素个数小于链表还原阈值 UNTREEIFY_THRESHOLD(6),
// 就会把桶中的树形结构缩小或者直接还原(切分)为链表结构
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 否则,就将 oldTab[j] 处的单链表分割为两个单链表
// 一个置于 newTab[j],另一个置于 newTab[j + oldCap] = hiHead;
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;
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;
}
}
}
}
}
return newTab;
}
对于扩容机制的实现,请先记住一个前提,因为数组的长度 length 总是为 2n,因此 hash&(length-1)
与 hash%length
等价。
然后用具有的实例,来讲解一下扩容的关键机制。
假设扩容之前,length = 16
(24),则对于数组 tab, tab[5] 对应的元素的 hash 值有:
(5) 101 & 1111
(21=5+16) 10101 & 1111
(37=5+16*2) 100101 & 1111
(53=5+16*3) 110101 & 1111
当 length = 16*2 = 32
(25)时 (新数组长度为扩容前的两倍) ,有 tab[5] 对应的元素为 5、37,tab[5+16] 对应的元素为 21、53。
e.hash & oldCap
,即 hash & 24( 10000(2) ) ,是为了判断第高 5 位是否为 1:
即与 hash & (25-1) (11111(2)) 的效果一致,当 length = 16
时,取的是低 4 位,当为 32 后,就要取低 5 位了,因此第 5 位的值(0或者1)
就影响着在新数组中的位置。
- 如果第 5 位为 1,则取低 5 的值比原来取的低 4 的值
5
( 101(2) )多了 16( 因为第 5 位为 1,则有 10000(2) ), 即有取低 5 的值 10101(2) - 如果第 5 位为 0,则取低 5 的值与原来取的低 4 的值
5
一致,不需要变化,即有取低 5 的值 00101(2)
因此在源码中有:newTab[j] = loHead;
与 newTab[j + oldCap] = hiHead;
这就是对 问题 3 扩容机制实现的关键点的理解。
另外,还有几个点需要补充下:
首先是 HashMap 中求 hash 值的算法:
// 对 key 求 hash 值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
如果 key 为空,则对应的哈希值直接为 0,否则就将 key 的 hashCode() 的值的高 16 位与低 16 位进行异或。
因为在映射到数组下标时,只取低 n 位,此时如果只是直接利用 hashCode() 的值的话,就会增大碰撞的几率,而将高低 16 位进行异或,就相当于将 hashCode() 的值的高位也参与了元算,就可以在一定程度上减少碰撞的几率,且因为是位元算,效率也较高。
补充: 哈希函数的构造方法 与 解决冲突的方法
https://blog.youkuaiyun.com/tian_110/article/details/43192595
以及 HashMap 中关于 hash 的几点冲突:
1、key 对应的对象的 hashcode() 方法不完善,导致不同的 key 生成相同的 hash 值,此时就需要借助 equals() 方法进行判断是否相等
2、不同 key 对应的 hash 值不同,但是映射的数组下标相同
以及,1.8 中与红黑树相关的内容,参见: