众所皆知map的底层结构是相似邻接表的结构,可是进入1.8以后,链表模式再必定状况下又会转换为红黑树
在JDK8中,当链表长度达到8,而且hash桶容量超过64(MIN_TREEIFY_CAPACITY),会转化成红黑树,以提高它的查询、插入效率底层哈希桶的数据结构是数组,因此也会涉及到扩容的问题。
当MyHashMap的容量达到threshold域值时,就会触发扩容。扩容先后,哈希桶的长度必定会是2的次方。html
那么为何用红黑树呢?以前都是用的链表,以前的文章有提到链表的随机访问效率是很低的,由于须要从head一个个日后面找,那么时间复杂度就是O(n),可是若是是红黑树由于红黑树是平衡二叉树,说白了就是能够索引的,那么时间复杂度只有O(logn),这样效率就能够获得很大的提升
也许有人就想问了,那为何还搞个链表啊,直接用红黑树不就完了:
1.链表比红黑树简单,构造一个红黑树要比构造链表复杂多了,因此在链表很少的状况下,总体性能上来看,当链表不长的时候红黑树的性能不必定有链表高
2.还有一个节点的添加和删除的时候,须要对红黑树进行旋转,着色等操做,这个就比链表的操做复杂多了
3.因此为链表设置一个阈值用来界定何时进行树化,何时维持链表,从中间取得一个均衡是很重要的node
刚刚讲到红黑树查找效率是O(logn)那么8的log是3,而使用链表,咱们以前也有提到,源码会进行折半查找(参考以前linkedlist源码分析)那就是8/2 = 4 平均查找长度是4,因此在8的时候是比较合适的由于3比4小
再好比链表长度为6的时候,红黑树会退化为链表同理:6=》log=2~3 和8相似,可是6/2=3也很快,并且红黑树很复杂,因此是用的链表,至于其中的数字7的做用是缓冲一下,避免再长度为7,8徘徊的时候会频繁修改成红黑树和链表
还有为何是64,参考网上记录是:再低于64的时候容量比较小,hash碰撞的概率比较大,这种时候出现长链表的可能性比较大,这种缘由致使的长链表咱们应该避免,而是采用扩容的策略避免没必要要的树化数组
接下来咱们观察一下hashmap的继承结构,了解一下数据结构
0.75f负载因子太高会致使链表过长,查找键值对时间复杂度就会增高,负载因子太低会致使hash桶的个数过多,空间复杂度变高函数
注意构造函数:源码分析
hash桶没有再构造函数中进行初始化,而是再第一次存储键值的时候进行初始化,initialCapacity返回一个大于等于初始化容量大小的最小2的幂次方post
1.插入数据的时候首先会判断hash桶是否为空,若是为空会进行初始化,这是避免调用构造函数以后没有数据致使,并且再初始化的时候会调用扩容策略这个后面再讲
经过刚刚的学习咱们知道hashmap有三种数据存放模式:数组,链表,红黑树
判断是否为空,若是为空,直接数组存放
这里有个细节性能
hash(key)和(n - 1) & hash 的使用
第一个对key进行hash取值学习
这里是由于hashcode是32位的数据,用hashcode和n相与的时候,若是n比较小,那么高位的数据基本就没用到(2的16次幂以上的数据),那么就会致使hash碰撞的几率加大
这里hash(key)的操做是吧hashcode右移16位在和原来的hashcode进行异或操做,至关因而吧高位的信息合并到低位上,而后在和n作与运算,这样高位低位的信息所有都有,综合的话hash碰撞的几率相应减低this
------------------------------------------------------------------------------------------------------------------------------------
说明一下,这两个操做都是取余操做,以前有人说是取模,这里科普一下,取模和取余是不同的
取模(百度百科):取模运算(“Module Operation”)和取余运算(“Complementation ”)两个概念有重叠的部分但又不彻底一致。主要的区别在于对负整数进行除法运算时操做不一样。取模主要是用于计算机术语中。取余则更可能是数学概念。模运算在数论和程序设计中都有着普遍的应用,从奇偶数的判别到素数的判别,从模幂运算到最大公约数的求法,从孙子问题到凯撒密码问题,无不充斥着模运算的身影。虽然不少数论教材上对模运算都有必定的介绍,但多数都是以纯理论为主,对于模运算在程序设计中的应用涉及很少。
7 mod 4 = 3(商 = 1 或 2,1<2,取商=1)
-7 mod 4 = 1(商 = -1 或 -2,-2<-1,取商=-2)
7 mod -4 = -1(商 = -1或-2,-2<-1,取商=-2)
-7 mod -4 = -3(商 = 1或2,1<2,取商=1)
R = a -c*b
好比-7 mod 4 => -7 = 1 -2 * 4
求模运算和求余运算在第一步不一样: 取余运算在取c的值时,向0 方向舍入(fix()函数);而取模运算在计算c的值时,向负无穷方向舍入(floor()函数)。
符号相同时,二者不会冲突。好比,7/3=2.3,产生了两个商2和37=3*2+1或7=3*3+(-2)。所以,7rem3=1,7mod3=1。符号不一样时,二者会产生冲突。好比,7/(-3)=-2.3,产生了两个商-2和-37=(-3)*(-2)+1或7=(-3)*(-3)+(-2)。所以,7rem(-3)=1,7mod(-3)=(-2)
------------------------------------------------------------------------------------------------------------------------------------
好的,咱们继续讨论(n-1)&hash和hash%n的问题
以前也有说到hashmap的扩容策略是大于等于初始化容量大小的最小2的幂次方,那么也就是说n是2的倍数,转换成2进制也就是最低位是0,再进行-1,那就是奇数
并且进行&操做
这里注意咱们的n是2的屡次幂,那么就是000100000000相似这样的二进制,减一的结果就是除了最高位其他一下都是1也就是:000011111111111
这个时候和原来的数据hash作&操做,就会把超出这个length范围的数据所有设置为0,也就是这个范围之内的数据不会变
Example:
8 =》 0000 0000 0000 1000
8 - 1 =》 0000 0000 0000 0111
而后不论什么数据与8-1作&操做,那么范围都在 0111以内,也就是7之内包含7范围再0~7,这样懂了吧,好比1000000&(7-1)结果就是0~7
固然出现这种状况有个必要的条件就是长度必须是2的n次幂,这样再二进制数列中,永远只有一个位置是1,其他位置是0,-1以后,这个位置一下的数据全包含再里面&就是截取低位的数据,吧高位去掉,至关因而取余了
由于不论什么数字都是x = a1*2^(n-1) + a2*2^(n-2) + … + a(n-1)*2^(1) + a(n)*2^(0),高位的确定都是2的y次幂的倍数,因此去掉倍数,剩下的就是余数,不知道我这么说你们有没有理解。。。
你们还能够看看我以前的博客:https://www.cnblogs.com/cutter-point/p/11091727.html
若是不为空那么就要进行链表化或者树化了
说白了就是再hash桶的数组上获取这个位置上的node节点,而后循环遍历获取到最后一个节点,而后插入到节点末尾
//链表存放 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { //链表尾部插入,p的next判断是否为空 p.next = newNode(hash, key, value, null); //当链表的长度大于等于树化阀值,而且hash桶的长度大于等于MIN_TREEIFY_CAPACITY,链表转化为红黑树 // if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st // treeifyBin(tab, hash); break; } //链表中包含键值对 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; }
红黑树的变换规则能够参考我以前的博客:http://www.javashuo.com/article/p-sdiixoyg-ke.html
咱们何时会进行树化呢???
就是当咱们的链表长度超过或等于8个的时候
至于如何吧这个链表组建为红黑树,这个之后单独开章节细细探讨。。。。
//数组扩容 public Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; //若是旧hash桶不为空 if (oldCap > 0) { ////超过hash桶的最大长度,将阀值设为最大值 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //新的hash桶的长度2被扩容没有超过最大长度,将新容量阀值扩容为之前的2倍 //扩大一倍以后,小于最大值,而且大于最小值 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) //左移1位,也就是扩大2倍 newThr = oldThr << 1; } else if (oldThr > 0) //若是旧的容量为空,判断阈值是否大于0,若是是那么就把容量设置为当前阈值 newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } //若是阈值仍是0,从新计算阈值 if (newThr == 0) { //当HashMap的数据大小>=容量*加载因子时,HashMap会将容量扩容 float ft = (float)newCap * loadFactor; //若是容量还没超MAXIMUM_CAPACITY的loadFactor时候,那么就返回ft,不然就是反馈int的最大值 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //hash桶的阈值 threshold = newThr; //初始化hash桶 @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; //若是旧的hash桶不为空,须要将旧的hash表里的键值对从新映射到新的hash桶中 if ((e = oldTab[j]) != null) { oldTab[j] = null; //只有一个节点,经过索引位置直接映射 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //取余 //若是是红黑树,须要进行树拆分而后映射 // else if (e instanceof TreeNode) // ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order //若是是多个节点的链表,将原链表拆分为两个链表,两个链表的索引位置,一个为原索引,一个为原索引加上旧Hash桶长度的偏移量 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; // 在遍历原hash桶时的一个链表时,由于扩容后长度为原hash表的2倍,假设把扩容后的hash表分为两半,分为低位和高位, // 若是能把原链表的键值对, 一半放在低位,一半放在高位,这样的索引效率是最高的 //这里的方式是e.hash & oldCap, //通过rehash以后,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。对应的就是下方的resize的注释 //为何是移动2次幂呢??注意咱们计算位置的时候是hash&(length - 1) 那么若是length * 2 至关于左移了一位 //也就是截取的就高了一位,若是高了一位的那个二进制正好为1,那么结果也至关于加了2倍 //hash & (length * 2 - 1) = length & hash + (length - 1) & hash if ((e.hash & oldCap) == 0) { //若是这个为0,那么就放到lotail链表 if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { //若是length & hash 不为0,说明扩容以后位置不同了 if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; //而这个loTail链表就放在原来的位置上 newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; //由于扩容了2倍,那么新位置就能够是原来的位置,右移一倍原始容量的大小 newTab[j + oldCap] = hiHead; } } } } } return newTab; }
总结就是扩容的时候吧数组大小扩大一倍,至关于左移1位,而且要从新计算hash散列值,找对应的位置填充
链表也要进行拆分,链表的拆分主要就体如今:
若是原来hash索引的位置就是这里,那么仍是链接再原来的节点上,若是取余到对应的位置的节点,数组扩大一倍,咱们原来的计算方式是hash&(n - 1)
那么若是咱们大小扩大一倍结果就是:hash&(2n - 1)=hash&n + hash&(n-1)由于n是2的n次幂,除了对应的位置为1其他位置都为0
那么这里就能够转换为hash&(2n - 1)=hash&n + hash&(n-1) => n + hash&(n-1) => oldIndex + oldCap 也就是旧索引位置加上旧的容量大小
查找对于红黑树部分咱们略过:
至于其余部分,也就是跟以前大同小异了,仍是hash取位置,而后取余获取对应的索引下标
首先检查是否是第一个,若是是那就直接返回了
若是不是循环遍历链表找到对应的key为止
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //注意这一步中(n - 1) & hash 的值 等同于 hash(k)%table.length if ((tab = table) != null && (n = tab.length) > 0 && //这里是计算至关因而取余的索引位置(n - 1) & hash 等价于hash % n //并且因为hashmap中的length再tableSizeFor的时候,就把长度设置为2的n次幂了,那么n-1以后的值,就是最高位全都是0,下面位数全是1 //这个也就是取hash的低位的值 (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; }
4.1 树形退化
红黑树,咱们就略过吧,这里篇幅有限不作探讨。。。。
这里能够讲讲hashmap的特殊地方了
1.hashmap是容许null键和值的,而hashtable就不容许了
参考:https://juejin.im/post/5a7719456fb9a0633e51ae14https://blog.csdn.net/xingfei_work/article/details/79637878https://juejin.im/post/5bed97616fb9a049b77fefbfhttps://www.zhihu.com/question/30526656https://juejin.im/post/5cb09c85e51d456e3428c0cf