HashMap的扩容

mac2025-04-20  5

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源码,写的很赞

最新回复(0)