我不想画图,直接盗图,啊哈哈!这个就是HashMap的结构了。
记住:先了解数据结构和算法,先了解数据结构和算法,先了解数据结构和算法。我们日常用到的很多东西,都是继续数据结构和算法的,你以为和你很远?
1、HashMap如何散列的?
hashMap是数组和单向链表的组合,把数据放在数组对应下边的链表中。
下面是HashMap中出现的2个计算数组下边的算法。
--》h & (length-1);
--》(h = key.hashCode()) ^ (h >>> 16)
拿第一个为例子,三个例子如下。(如果连hashCode都不知道,先回去补一补这个)
78612533 & (10 - 1)
100 1010 1111 1000 1000 0011 0101
1001
78612 & (10 - 1)
1 0011 0011 0001 0100
1001
786123 & (10 - 1)
1011 1111 1110 1100 1011
1001
看出来了吧?&操作,让结果不会超出10 - 1的范围,其实就是利用key的hashCode值最后的位数和1001 &操作,来分配数组的下边的,然后把数据放在下边对应的链表中。
总结:不管,数组长度如何变化,通过&操作,都可以将不同的hash归类。
*** 这里很重要,数组长度变化了,根据hashCode计算出来的数组下边也许也会变化,那边存储在数组下的链表就需要重组。这个很重要。这个跟扩容原理有关系。
2、如何扩容的?
看了源码,也对比了别人的学习笔记,我整理出通俗易懂的东西出来。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; // 备份下当前的数组
int oldCap = (oldTab == null) ? 0 : oldTab.length; // 记录下当前数据的长度
int oldThr = threshold; // 这个是阈值 作用看看下面的oldCap >= MAXIMUM_CAPACITY就知道了,threshold的计算,看下tableSizeFor方法
int newCap, newThr = 0; // 新的数组长度和阈值大小
if (oldCap > 0) { // 这里不比说,判断当前数组
if (oldCap >= MAXIMUM_CAPACITY) { // 如果数组长度,已经大于当前HashMap设置的最大容量时,1 << 30
threshold = Integer.MAX_VALUE; // 再次扩容,就直接Integer的最大值给你了。
return oldTab; // 将当前的数组返回,某些计算需要用到
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY) // newCap = oldCap << 1 这里的计算,让新的数组长度比老的翻了一倍,
newThr = oldThr << 1; // double threshold // 都有备注了,将阈值也翻倍
}
else if (oldThr > 0) // initial capacity was placed in threshold // 走到这里,是在当前数组的长度==0的时候,
newCap = oldThr; // 把当前的阈值长度给到新数组的长度,
else { // zero initial threshold signifies using defaults // 如果当前的 数组长度和阈值都为0
newCap = DEFAULT_INITIAL_CAPACITY; // 默认值给他
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 阈值的大小,就是 最小长度*荷载系数,默认的荷载系数是0.75f,
}
if (newThr == 0) { // 若到了这里,新的阈值还是为0?
float ft = (float)newCap * loadFactor; // 那先算出新的阈值,
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? // 如果计算出来的阈值大于了MAXIMUM_CAPACITY,那就用Integer的最大值。
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr; // 好了,先把算出来新的阈值给到全局阈值
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 搞一个新的数组,这里的数组长度,不出意外,就是之前的2倍了。
table = newTab; // 当前的数组已经在上面备份起来了,这里先把新的数组给到全局,我这就开始往里头加东西了。
if (oldTab != null) { // 当前备份起来的数组,不为null时
for (int j = 0; j < oldCap; ++j) { // 开始循环当前的数据
Node<K,V> e; // 建立个新节点,等下要用
// 如果数组的第一个节点不为空,那就赋值给上面创建的节点e。记住,这里的节点后面的数据是个单向链表,
//
if ((e = oldTab[j]) != null) {
oldTab[j] = null; // 那当前数组的j下边置空
if (e.next == null) // 如果这个节点就只有一个自己,没有next
// 这里直接通过e.hash计算出在新数组中的位置,然后把e丢过去。也许你会想,为什么有这么个判断呢?
// 回到上面HashMap是如何离散的,你就明白了。当数组长度变化了,key的hash值&数组长度得出来的新数组的下标也变化了。
// 这个判断拿出来玩,只是简述了只有一个的特殊情况,重点还在这个判断的最后面的else
newTab[e.hash & (newCap - 1)] = e; // 链表长度不够,没有转成树,这里只能是链表对象。
else if (e instanceof TreeNode) // 这里也是非常重要的一点,你在源码中搜索下TREEIFY_THRESHOLD就知道了。HashMap有个变身阈值,当大于TREEIFY_THRESHOLD,就会把单向链表转成树
((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 树这块大家留言,如果有必要,我再开一篇讲下。
else { // preserve order // 这里就是把当前数组的j下标下的链表数据,转移
Node<K,V> loHead = null, loTail = null; // 这里为什么有这么多的节点呢?
Node<K,V> hiHead = null, hiTail = null; // 这里为什么有这么多的节点呢?
Node<K,V> next; // 这个是游标,循环保存数据用的
do {
next = e.next; // 把e的下一个节点备份下,因为下一次循环,会用到,因为要通过这个next找下一个,
if ((e.hash & oldCap) == 0) { // 这里为什么要用oldCap?为什么又不减1呢? 很多人迷茫在这里,标注1
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
// 经过高低位区分,把数组下标为j的链表都分到了2个不同的2个链表中,这里这里,如果不明白,你就没明白这个算法的精妙
// 我再说明一点,你就明白为什么用高低位分了。假设当hash&oldCap的时候,hash参与的是0001,1001等10种,当新数组再次排列的时候
// 因为扩容的原因,hash的参与的0001,1001等前一位将参与到&计算中,这样一定会把全部的hash分成2中情况。不可能出现其他情况的。
// 这下明白了为什么要分高低位了吧。
} while ((e = next) != null);
// 然而,经过上面的分离,我们就可以2个不同链表分在不同的数组中,如果高低位都有的话,
// 那一定是一个高位一个低位,不可能有例外
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 标注1:原来,新旧数组被以oldCap分为高位和地位。这个就是为上面会出现lo和hi开头的Node了。
// 这里(e.hash & oldCap) == 0,就是关键了。可以这么理解,hash还是之前数据的hash值,hash &上的值,比之前大了1(原本的计算下标的位置是oldCap - 1),
// 也就是说,如果之前是16(10000),那之前计算下标的时候,就是hash&15(1111),这个时候hash值变化了,结果都会在0-15的下标数组中。这个毋庸置疑的。
好了,看黑板。重点来了,看看他是这么玩的,怎么重组的。
// 以16的扩容场景来看,hash&15(1111)时,hash值和他&,都是只有4位的,记住这个,这个和为标注1要用(e.hash & oldCap)来区分高低位了。
// hash&15(1111)时 hash值可以是0001,0002,1001等16种情况,这个时候,一定是16种,那问题来了。hash的二进制长度肯定不止4位啊,那在0001,0002前面一位是什么呢?
// 大家知道了,只能是0或1,那标注1用(e.hash & oldCap)就是hash&16(10000)结合上面的分析,如果hash的前一位是0,那这个结果算出来的,(e.hash & oldCap) == 0是成立的
// 如果前一位是1,那(e.hash & oldCap) != 0。
// 到这里了,认真看过的同学都知道了,如果新的数组中,出现(e.hash & oldCap) == 0的数据,他就出在lo区域;如果不等于0.说明在新的数组中,他可以处于高位。
// 最后的总结:扩容,就是把当前数组中的属于新数组新加进来的下标的链表分离出来,形成一个链表并保存起来。
// 此时此刻,扩容原理完成了。
return newTab;
}