上一篇对HashMap的结构做了详细的介绍,讲解了put方法还有get方法,本篇将会更深入的走进HashMap源码。
散列函数(解释hash碰撞)
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
在上篇文章我们讲解的get和put方法中都使用了hash(key),现在让我们一起来看下这个hash函数究竟是什么吧。
/**
* 将高位与低位进行与运算来计算哈希值。因为在hashmap中使用2的整数幂来作为掩码,所以只在当前掩码之上的位上发生
* 变化的散列总是会发生冲突。(在已知的例子中,Float键的集合在小表中保持连续的整数)因此,我们应用一个位运算
* 来向下转移高位的影响。 这是在综合考虑了运算速度,效用和质量之后的权衡。因为许多常见的散列集合已经合理分布
* (所以不能从扩散中受益),并且因为我们使用树来处理bin中发生的大量碰撞的情况,所以我们尽可能以代价最低的方式
* 对一些位移进行异或运算以减少系统损失, 以及合并由于hashmap容量边界而不会被用于散列运算的最高位的影响。
*/
//将key的hashCode的高16位与低16位做了一个异或操作
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

n是table的大小,默认是16,二进制即为10000,n - 1 对应的二进制则为1111,这样再与hash值做“与”操作时,就变成了掩码,除了最后四位全部被置为0,而最后四位的范围肯定会落在(0~n-1)之间,正好是数组的大小范围,散列函数的妙处就在于此了。

就拿昨日我们的例子做实验进行计算:
“噢噢”的hash值为707658,转化为二进制就是:10101100110001001010,table大小是32,n-1也就是31对应的二进制则是:11111

运算之后的结果是:01010,也就是10,所以“噢噢”的下标就落在了table的10号位
再来看“啊啊”,它的hash值为698698,对应的二进制是10101010100101001010,table大小是32,n-1也就是31对应的二进制则是:11111

运算之后的结果是:01010,也同样是10号位,因此发生了hash碰撞,形成了链表。
扩容函数
在之前我们看过了HashMap中put方法的源码,我们可以经常看到resize()这个方法,这个方法是用来对hashmap进行扩容的,现在让我们一块来对其分析一下
/**
* 初始化或将table的大小进行扩容。 如果table为null,则按照字段threshold中的初始容量目标进行分配。
* 否则,因为我们使用2次幂进行扩容,所以在新表中,来自每个bin中的元素必须保持在相同的索引处,或者以原偏移量的2次幂进行移动。
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//扩容前table容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//扩容前阈值
int oldThr = threshold;
int newCap, newThr = 0;
//如果旧容器的容量不为0
if (oldCap > 0) {
//如果扩容前的容量大于等于HashMap的最大容量,则将其阈值设置为2的31次方减1(MAXIMUM_CAPACITY的值为2的30次方,永远达不到阈值,不再对其进行扩容),并返回原容器
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//新容器调整为原来的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//新容器的阈值也调为之前的两倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) //旧容器容器容量为0,但是阈值不为0时,初始容量被设置为阈值
newCap = oldThr;
else { // 旧容器容器容量为0,阈值为0时,使用默认出初始化容器大小与阈值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
//旧容器容器容量为0,但是阈值不为0时,此时新容器大小被赋予上了阈值的大小
//给新容器设置新阈值
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//将旧数组中的node重新散列到新数组中
@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) {
//节点不为null
oldTab[j] = null;
if (e.next == null)
//如果该节点为没有子节点,则直接放入到新table中
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//如果该节点为树节点,则对树的hash重新分配
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
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;
//如果该节点为新容器的0号位
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;
}
总结:HashMap使用的是懒加载的方式,在构造函数中并没有初始化table,而是再添加第一个数据的时候在扩容方法中初始化了table。
当使用put插入元素的时候,如果发现目前的bins占用程度已经超过了加载因子所设置的比例,那么就会发生resize,简单来说就是把原来的容量和阈值都调整为原来的2倍,之后重新计算数组索引,把节点再放到新的bin中。因为index值的计算与table数组的大小有关,所以扩容后,元素的位置有可能会调整。
拿之前的“订单”为例,在没有扩容之前:


经过计算在容量为8的时候的确在2号位上
但是,在扩容到size为64的时候我们再计算一下它的位置

经过计算它应该在34号位置上,而事实也正是如此,扩容后元素的位置发生了改变。

网友评论