我们先来看一篇故事,有益我们有更好的理解。
很久以前有一个学校,有7500名学生(size)
和100间教室(oldCap)
。学生的学号为0000~7499(hash)
,教室的编号为0~99 (数组下标)
。为了使学生均匀的进入每间教室,校长规定,按学生学号的末尾2位选择对应编号的教室(hash & (数组长度 - 1))
,如:
- 学号 0001 进入 01 教室
- 学号 0012 进入 12 教室
- 学号 0212 进入 12 教室
- 学号 7001 进入 01 教室
- 学号 7401 进入 01 教室
学校规定:如果学生超过7500名(超过 oldThr)
,就将教室扩充,扩充 10 倍,即1000间(newCap)
。将1000间分为10个区(原来的区对应低位,多出来的9个区对应高位)
。
(每个区的每个房间教室摆放一摸一样,只有编号不一样)
- 0区 0~99
- 1区 100~199
- 2区 200~299
- 以此类推
(因为 HashMap 计算是2进制,所以扩充2倍 , 而我们这里计算为 10 进制,所以扩充10倍。为了更方便理解。)
不巧过几天就来了一名新学生,校长也如约的将教室扩充到了1000间。扩充完毕后就需要重新分配位置了,这次的规定是,按学生学号的末尾3位选择对应编号的教室,如:
- 学号 0001 进入 001 教室
- 学号 0012 进入 012 教室
- 学号 0212 进入 212 教室
- 学号 7001 进入 001 教室
- 学号 7401 进入 401 教室
对于教室扩充为1000间,并将匹配的学号尾数由2位变成了3位。可以看到,学号第二位(从左往右数)为0的学生,教室位置根本就没有变化。
所以校长为了加快换教室的速度,指挥到:“学号第二位是0的学生坐在原位不要动((e.hash & oldCap) == 0)
,不为0的学生((e.hash & oldCap) != 0)
,根据第二位的数字找到相应的区,同样位置的教室坐下!"。此话一出,学生快速找到了自己对映的区和对映的教室,不一会就各自在自己的位置上坐好了。
上面的故事抽象的描述了一下 HashMap 扩容时候的操作。最主要的就是在校长指挥的时候,判断学号第二位是否为0,为0就在原地不动,不为零加上相应的固定值(上面故事中固定值是自己学号的第二位固定值)。
所以要理解 HashMap 如何扩容,必须深刻的理解 HashMap 是如果通过 HashCode 进行计算得出存储位置的。
下面我们再来看图,看看是如何进行扩容的。
我们要先知道16的二进制是 00000…0010000.和以下几个需要明白的地方
- 一个16容量的 HashMap 数组,阀值是12,在存入第13个的时候开始扩容。
- 需注意 HashCode 是 32 位的字节码,但是在存储的时候其实一般关注后面的位数
- 黑色 bit 位 表示本次计算存储位置时需要用到的 bit 位,
红色 bit 位 的表示本次计算存储位置时不需要考虑是什么值的 bit 位- 每个链表的元素都分为两种颜色,一种深色,一种浅色。
4.1. 深色元素代表本次扩容时,容量值(16)的二进制是1的坐标(16的2进制为1的,坐标是第5位)和 hash 相应坐标的值相同 (第5位为1)
4.2. 浅色则和 hash 相应坐标的值不同(第5位为0)
可以看到,扩容后,重新计算位置时,同一个链表中,浅色元素全部留在了原来的位置,
深色元素全部都到了 (当前坐标 + 原来的的容量值)的位置。即:
- 原来在 0 的去 0 + 16 = 16
- 原来在 3 的去 3 + 16 = 18
- 原来在 15 的去 15 + 16 = 31
这个就是 HashMap 扩充的特性。在相同链表的扩充后存储位置只有两种选择,要不留在原地,要不加上相应的原容量值。这也是 HashMap 扩容时最重要的环节。也是最耗时的地方(但是对于别的解决办法来说是最快的最高效的)。
最后附上流程图,看不清楚可以右键点击放大观看。
如果有那些讲的不好或者讲的不仔细的地方,欢迎评论留言。
Map (一) HashMap 构造函数的秘密
Map (二) HashMap put()方法详细解刨
Map (三) HashMap 如何利用 hash 计算存储位置
Map (四) HashMap 已故事的角度理解 resize()
Map (五) HashMap get()