【源】终于明白JDK8 HashMap底层数组长度,取值2次幂的缘由

jdk1.8中的hashmap做了不少改进:红黑树的引入,链表尾插,以及底层数组长度保持2的次幂。本文专一于分析2次幂设定的缘由,且听我慢慢道来……算法

与“取余”等价的算法

众所周知,hashmap是数组链表结构:hash算法用于将key散列,经计算分散到数组槽中;而两个key算出了一样的值,即产生hash冲突时,就须要将槽中的单个节点升级成链表。因为get时须要对链表其进行遍历,链表越长检索效率越差。那么,计算出的key值落点越平均,hash冲突的可能性越小数组

key值落点的计算方式为,key的hash值数组长度取余操做,记做key.hascode % array.length。从数学角度考虑,保持array.length为质数会使得计算结果更均衡,hashTable就是这么作的(数组初始值11)。但 hashmap 中 array.length 恰恰选择了2的次幂,是个合数……何故?彻底出于性能考虑!
先给出结论——当 array.ength长度是2的次幂时,key.hashcode % array.length等于key.hashcode & (array.length - 1)。下面重点看下这个结论是怎么得出来的。性能

举个例子:
假如 array.length = 2^4 = 16,二进制10000。这个数减去1的结果是1111,也就是array.length -1 = 1111。
(下面这段中的数字都是二进制)
再假设一个key的值为10011011001(很随意写的一个数),与1111作 & 操做,获得的结果是1001(高位部分1001101都舍去了)。而1001必然是一个小于10000的数,对于一个小于10000的数而言,1001 % 10000获得的就是1001本身。
那么刚刚舍弃的高位部分1001101 0000(后面补上了四个0000)就必定能被10000整除吗?答案是确定的:由于10011010000能够拆成10000000000+10000000+1000000+10000,这几个数都能经过10000的n次左移获得,也就至关于这几个数都能被10000整除。那他们的和,也就是10011010000,必定也能够被10000整除。
所以,最终结论就是:10011011001 & ( 10000 - 1 ) = 10011011001 & 1111 = 1001 = 10011011001 % 10000spa

放张简图再唠叨一遍以示总结,加深下印象:
clipboard.png3d

再强调一次:当 array.ength长度是2的次幂时,key.hashcode % array.length等于key.hashcode & (array.length - 1)code

好,若是你读懂了例子部分,相信你已经基本明白这个结论是站得住脚的(虽然不是纯数学型的讲解)。那么hashmap的做者Doug Lea大神,为何如此执着于用&操做替换%操做呢?
由于对于二进制生物计算机来讲,& 的效率要高于 %!(与、或、非均可看做二进制基本操做,同或、异或次之,+ - * ÷ % 等都基于前面的)blog

扩容时方便定位

这还不算完,好处不止这一处。
当hashmap须要扩容,从新计算链表元素的hashcode,以进行元素的从新定位时,依然能从“ 数组2次幂 ”的这个设定中借力!索引

hashmap数组扩容时,新数组length = 原数组length * 2,沿用前面的例子(array.length = 2^4 = 16,二进制10000),array.length 乘以 2 ,即二进制左移一位,由 10000 变成 100000。此时须要从新计算数组槽中的元素位置,若是槽中是链表,链表中每一个元素都须要从新计算位置(这里不考虑红黑树)。ip

计算的公式不变,key.hashcode & (array.length - 1),因为数组的翻倍(10000->100000),致使 array.length - 1 发生了改变(1111->11111)。此时,扩容前本来被舍弃的高位部分的最后1位,也将参与计算。
clipboard.pngget

在扩容这个历史的拐点,这一位就显得很特别:若是这个位置是0,余数计算的结果将保持不变,意味着扩容后此元素还在这个槽中(槽编号没发生改变);若是这个位置是1,余数计算结果就变成了原槽索引 + 原array.length
也就是说,hashmap扩容的元素迁移过程当中,因为数组大小是2次幂的巧妙设定,使得只要检查 “ 特殊位 ” 就能肯定该元素的最终定位。

给出一个较完整的扩容示意图进行说明:
clipboard.png

  • 扩容前

红绿黄三个元素,由各自的hashcode取余后都淤积在数组槽13,组成链表形式

  • 扩容后

红、绿二星所表示的元素的hashcode“ 特殊位 ”为0,取余依然定位在槽13;而黄星表示的元素,hashcode“ 特殊位 ”为1,取余后结果 = 原槽索引 + 原数组大小 = 13 + 16 = 29。(这个结果也和图中黄星的hashcode二进制低位值11101一致)

总结

对hashmap而言,数组长度始终保持2次幂有两点好处:

  1. 能利用 & 操做代替 % 操做,提高性能
  2. 数组扩容时,仅仅关注 “特殊位” 就能够从新定位元素

性能,性能,仍是性能……

相关文章
相关标签/搜索