HashMap的扩容

HashMap默认数组大小是16,研究表明,当数组长度为2的n次幂的时候,不同的key算得的index相同的几率较小,数据在数组上分布的比较均匀,也就是产生hash碰撞的几率比较小,相对的,数据存放在链表上的几率比较小,查询效率也就比较高了。
所以在存储大数量数据的时候,最好指定hashmap的size为2的整数次幂,就算不指定的话,也会以大于且接近于指定值大小的2次幂来初始化的。预先指定的话,可以减少resize次数,提高效率。

hashmap在什么时候会进行扩容呢?
如果没有预先指定hashmap的大小的话,初始化的时候会默认数组大小为16,负载银子为0.75,当hashmap中存放的数据个数超过数组大小*loadFactor时,就会进行数组扩容,扩大一倍;例如当map中数据数量大于16*0.75=12的时候,就会把数组大小扩展为2*16=32个,然后重新计算每个元素在数组中的位置,重新散列存放。这是一个非常消耗性能的过程,所以如果我们能预知hashmap中元素的个数,预置好可以提高hashmap的性能。
比如我们知道元素个数1000,那么应该预置为:length*0.75>1000;而且保证length是2的n次幂,所以length应该设置为2048,new HashMap(2048)。

key如果是自定义对象,那么一定要重写equals和hashcode方法。

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

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

扩容源码:

JDK1.7

void resize(int newCapacity) {   //传入新的容量  
    Entry[] oldTable = table;    //保存旧的entry数组,引用扩容前的Entry数组  
    int oldCapacity = oldTable.length;//保存旧的容量  
    if (oldCapacity == MAXIMUM_CAPACITY) {  //扩容前的数组大小如果已经达到最大(2^30)了  
        threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了(容量达到了最大值)  
        return;  
    }  
  
    Entry[] newTable = new Entry[newCapacity];  //初始化一个新的Entry数组  
    transfer(newTable);                         //!!将数据转移到新的Entry数组里  
    table = newTable;                           //HashMap的table属性引用新的Entry数组  
    threshold = (int) (newCapacity * loadFactor);//修改阈值 -- 产生新的阈值 
}  

    /**
     * Transfers all entries from current table to newTable.
     */
   void transfer(Entry[] newTable) {  
       Entry[] src = table;             //src引用了旧的Entry数组  
       int newCapacity = newTable.length;//计算新的数组容量  
       for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组  
         Entry<K, V> e = src[j];             //取得旧Entry数组的每个元素  
         if (e != null) {  
         src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)  
             do {  
                  Entry<K, V> next = e.next;//每个entry都包含一个next指针, 头插式 
                  int i = indexFor(e.hash, newCapacity); //!重新计算每个元素在数组中的位置  
                  e.next = newTable[i]; //标记[1]  
                  newTable[i] = e;      //将元素放在数组上  
                  e = next;             //访问下一个Entry链上的元素  
            } while (e != null);  
        }  
    }  
}  

 static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

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

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

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

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

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

发布了23 篇原创文章 · 获赞 0 · 访问量 2936

猜你喜欢

转载自blog.csdn.net/kris_lh123/article/details/102854541