HashMap的扩容机制---resize()

来源:互联网 发布:手机怎么查淘宝信誉度 编辑:程序博客网 时间:2024/05/22 15:27

虽然在hashmap的原理里面有这段,但是这个单独拿出来讲rehash或者resize()也是极好的。

什么时候扩容:当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值—即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。

扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。

我们分析下resize的源码,鉴于JDK1.8融入了红黑树,较复杂,为了便于理解我们仍然使用JDK1.7的代码,好理解一些,本质上区别不大,具体区别后文再说。

  1. void resize(int newCapacity) {   //传入新的容量  
  2.     Entry[] oldTable = table;    //引用扩容前的Entry数组  
  3.     int oldCapacity = oldTable.length;  
  4.     if (oldCapacity == MAXIMUM_CAPACITY) {  //扩容前的数组大小如果已经达到最大(2^30)了  
  5.         threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了  
  6.         return;  
  7.     }  
  8.   
  9.     Entry[] newTable = new Entry[newCapacity];  //初始化一个新的Entry数组  
  10.     transfer(newTable);                         //!!将数据转移到新的Entry数组里  
  11.     table = newTable;                           //HashMap的table属性引用新的Entry数组  
  12.     threshold = (int) (newCapacity * loadFactor);//修改阈值  
  13. }  
这里就是使用一个容量更大的数组来代替已有的容量小的数组,transfer()方法将原有Entry数组的元素拷贝到新的Entry数组里。
  1. void transfer(Entry[] newTable) {  
  2.     Entry[] src = table;                   //src引用了旧的Entry数组  
  3.     int newCapacity = newTable.length;  
  4.     for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组  
  5.         Entry<K, V> e = src[j];             //取得旧Entry数组的每个元素  
  6.         if (e != null) {  
  7.             src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)  
  8.             do {  
  9.                 Entry<K, V> next = e.next;  
  10.                 int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置  
  11.                 e.next = newTable[i]; //标记[1]  
  12.                 newTable[i] = e;      //将元素放在数组上  
  13.                 e = next;             //访问下一个Entry链上的元素  
  14.             } while (e != null);  
  15.         }  
  16.     }  
  17. }  
  1. static int indexFor(int h, int length) {  
  2.     return h & (length - 1);  
  3. }  
文章中间部分:四、存储实现;详细解释了为什么indexFor方法中要h & (length-1)

newTable[i]的引用赋给了e.next,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置;这样先放在一个索引上的元素终会被放到Entry链的尾部(如果发生了hash冲突的话),这一点和Jdk1.8有区别,下文详解。在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上。

下面举个例子说明下扩容过程。

这句话是重点—-hash(){return key % table.length;}方法,就是翻译下面的一行解释:

假设了我们的hash算法就是简单的用key mod 一下表的大小(也就是数组的长度)。

其中的哈希桶数组table的size=2, 所以key = 3、7、5,put顺序依次为 5、7、3。在mod 2以后都冲突在table[1]这里了。这里假设负载因子 loadFactor=1,即当键值对的实际大小size 大于 table的实际大小时进行扩容。接下来的三个步骤是哈希桶数组 resize成4,然后所有的Node重新rehash的过程。

jdk1.7扩容例图

下面我们讲解下JDK1.8做了哪些优化。经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,

经过rehash之后,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。对应的就是下方的resize的注释。

[java] view plain copy
  1. /** 
  2.  * Initializes or doubles table size.  If null, allocates in 
  3.  * accord with initial capacity target held in field threshold. 
  4.  * Otherwise, because we are using power-of-two expansion, the 
  5.  * elements from each bin must either stay at same index, or move 
  6.  * with a power of two offset in the new table. 
  7.  * 
  8.  * @return the table 
  9.  */  
  10. final Node<K,V>[] resize() {  

看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。

hashMap 1.8 哈希算法例图1

元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:

hashMap 1.8 哈希算法例图2

因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图:

jdk1.8 hashMap扩容例图

这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。有一点注意区别,JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上图可以看出,JDK1.8不会倒置。有兴趣的同学可以研究下JDK1.8的resize源码,写的很赞,如下:

 1 final Node<K,V>[] resize() { 2     Node<K,V>[] oldTab = table; 3     int oldCap = (oldTab == null) ? 0 : oldTab.length; 4     int oldThr = threshold; 5     int newCap, newThr = 0; 6     if (oldCap > 0) { 7         // 超过最大值就不再扩充了,就只好随你碰撞去吧 8         if (oldCap >= MAXIMUM_CAPACITY) { 9             threshold = Integer.MAX_VALUE;10             return oldTab;11         }12         // 没超过最大值,就扩充为原来的2倍13         else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&14                  oldCap >= DEFAULT_INITIAL_CAPACITY)15             newThr = oldThr << 1; // double threshold16     }17     else if (oldThr > 0) // initial capacity was placed in threshold18         newCap = oldThr;19     else {               // zero initial threshold signifies using defaults20         newCap = DEFAULT_INITIAL_CAPACITY;21         newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);22     }23     // 计算新的resize上限24     if (newThr == 0) {25 26         float ft = (float)newCap * loadFactor;27         newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?28                   (int)ft : Integer.MAX_VALUE);29     }30     threshold = newThr;31     @SuppressWarnings({"rawtypes","unchecked"})32         Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];33     table = newTab;34     if (oldTab != null) {35         // 把每个bucket都移动到新的buckets中36         for (int j = 0; j < oldCap; ++j) {37             Node<K,V> e;38             if ((e = oldTab[j]) != null) {39                 oldTab[j] = null;40                 if (e.next == null)41                     newTab[e.hash & (newCap - 1)] = e;42                 else if (e instanceof TreeNode)43                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);44                 else { // 链表优化重hash的代码块45                     Node<K,V> loHead = null, loTail = null;46                     Node<K,V> hiHead = null, hiTail = null;47                     Node<K,V> next;48                     do {49                         next = e.next;50                         // 原索引51                         if ((e.hash & oldCap) == 0) {52                             if (loTail == null)53                                 loHead = e;54                             else55                                 loTail.next = e;56                             loTail = e;57                         }58                         // 原索引+oldCap59                         else {60                             if (hiTail == null)61                                 hiHead = e;62                             else63                                 hiTail.next = e;64                             hiTail = e;65                         }66                     } while ((e = next) != null);67                     // 原索引放到bucket里68                     if (loTail != null) {69                         loTail.next = null;70                         newTab[j] = loHead;71                     }72                     // 原索引+oldCap放到bucket里73                     if (hiTail != null) {74                         hiTail.next = null;75                         newTab[j + oldCap] = hiHead;76                     }77                 }78             }79         }80     }81     return newTab;82 }
0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 2岁宝宝头发稀少怎么办 掉头发很厉害怎么办吧 头发点的很厉害怎么办 为什么掉头发很厉害怎么办 产后2年脱发严重怎么办 产妇掉头发很厉害怎么办 头发掉了怎么办小妙招 头顶头发掉的厉害怎么办 20岁头发有些少怎么办 头痒头发掉厉害怎么办 头油头痒掉头发怎么办 头发痒掉发严重怎么办 宝宝的脸皴了怎么办 宝宝冬天脸皴了怎么办 白衬衣领子变黄怎么办 白衬衣领子烂了怎么办 玩手机眼睛红了怎么办 吃了发芽的土豆怎么办 散尾竹叶子发黄怎么办 吃了一朵长春花怎么办 3d模型打开缓慢怎么办 多肉叶片发芽后怎么办 多肉种子发芽后怎么办 玫瑰金手机掉漆怎么办 18k金掉色了怎么办 6s玫瑰金掉漆了怎么办 书被水泡了皱了怎么办 长头发掉的厉害怎么办 长头发容易掉发怎么办 家人被传销洗脑怎么办 衣服上有荧光剂怎么办 有荧光剂的衣服怎么办 3个月了恶露还有怎么办 京东倒闭欠的钱怎么办 便利通卡过期了怎么办 京东e卡被绑定了怎么办 天猫预售不发货怎么办 面签失败首付款怎么办 华为荣耀8卡顿怎么办 荣耀3c开不了机怎么办 华为3c开不了机怎么办