HashMap的扩容


当HashMap的size达到临界值capacity * loadFactor - 1时,HashMap会进行扩容,将自身容量增加一倍。
比如对未指定capacity和loadFactor的HashMap,缺省容量和负载因子分别为16和0.75,因此当map中存储的元素数量达到16 * 0.75 - 1即为11时,该map会将自身容量扩大到2 * 16 = 32。
在某些情况下,扩容耗费较多时间。如下面的代码

    public static void main()
    {
        HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
        for (int i = 0; i < 1200000; i += 3)
        {
            map.put(i, i + 1);
        }
    }

  JProfiler的统计信息如下,可以看到扩容的耗费在transfer方法上。

transfer功能是创建一个新的table并将map中现有的元素依次拷贝到新table中。上例中map中最终元素数为40万,扩容了16次,每次扩容需要transfer的数据数目分别是16 * 0.75 - 1 = 11, 32 * 0.75 - 1 = 23, 64 * 0.75 - 1 = 47, 128 * 0.75 - 1 = 71, ... ... , 262016 * 0.75 - 1 = 196511, 524032 * 0.75 - 1 = 393023


    为了在数据量较多时避免扩容,可以在创建HashMap时指定capacity。capacity的计算示意如下

如果map的最终元素数量落在如下的左面区间,那么HashMap不会扩容,否则会扩容。


        首先说明一下HashMap的构造函数决定容量的方式
        HashMap的构造函数自动会根据提供的initialCapacity寻找大于等于initialCapacity的并且是2的指数幂的最小数值。
        比如new HashMap(17)new HashMap(31)创建的HashMap的容量都是32,但我们知道capacity为32的map扩容临界值为32 * 0.75 - 1 = 23,因此若最终map会放入31个元素,map会在放入第23个元素的时候将容量从32扩大到64。
        创建HashMap时指定的capacity有两种计算方式,假定map容纳的元素数最终是数值m
那么

    方式一
    new HashMap(m * 4 / 3 + 3)

创建的map不会扩容
原因是对临界值m = 2^n * 0.75 - 1,m * 4 / 3 + 3结果2^n + 5/3取整为2^n + 1,而HashMap构造器的参数为2^n + 1时其capacity会是2^(n + 1),举例如下
22个元素对应的值22 * 4 / 3 + 3取整结果为32;23个元素对应的值22 * 4 / 3 + 3取整结果为33,new HashMap(33)创建的map最终容量是64.

    方式二

是直接找出大于m * 4 / 3 + 3的并且是2的指数幂的最小数值,比如m为17时,找出 17 * 4 / 3 + 3 = 25并且是2的指数幂的值为32,示例函数如下

int getMinCapacityNotResize(int targetEntrySize)
{
    if (targetEntrySize <= 0)
    {
        return 0;
    }
    double exponent = Math.log(targetEntrySize) / Math.log(2);
    double defaultInitCapacity = Math.pow(2, Math.ceil(exponent));
    double capacity = defaultInitCapacity;
    if (targetEntrySize >= defaultInitCapacity * 3 / 4 - 1)
    {
        capacity = defaultInitCapacity * 2;
    }
    return (int) capacity;
}

<< `HashMap` 是 Java 集合框架中的重要组成部分之一,用于存储键值对(key-value pairs)。当 `HashMap` 中的数据量超过其容量限制时,就需要进行扩容操作。下面是关于 `HashMap` 扩容机制的具体介绍。 ### HashMap扩容过程 1. **初始状态**: - 当创建一个新的 `HashMap` 对象时,默认的初始化容量为 16(即底层数组大小为 16),加载因子默认为 0.75。 - 公式:threshold = 容量 * 加载因子 ```java int threshold = (int)(capacity * loadFactor); ``` 初始阈值为 12 (`16 * 0.75`),这意味着如果当前元素数量达到或超过该阈值,则触发扩容操作。 2. **扩容时机**: - 每次向 `HashMap` 插入新数据之前都会检查是否满足以下条件: 如果 `size >= threshold` (其中 size 表示实际包含的有效键值对数目),则执行扩容逻辑。 3. **扩容步骤**: - 创建新的更的底层数组。通常情况下会将原数组长度翻倍,并重新计算 hash 值以分配到新位置上。(假设原来 capacity=16, 新容量变为 32) - 将旧表中的所有条目逐一迁移到新的数组中去。 4. **迁移过程中可能遇到的问题及处理方式**: - 在 JDK8 及之后版本里优化了链表结构,在量冲突的情况下使用红黑树替代普通单向链表形式;此外还有其他细节上的改进如 CAS 循环等保证线程安全等问题不再赘述。 ```java import java.util.HashMap; public class Test { public static void main(String[] args) throws Exception{ // 构造一个空的HashMap实例 HashMap<Integer,Integer> map=new HashMap<>(8,(float)0.5); System.out.println("Initial Threshold:"+map.threshold()); for(int i=0;i<9;i++){ if(i==8){ System.out.println("\nBefore putting key "+i+", table is :"); printTable(map.table); //Put will trigger resize here since current entries count reaches to the initial threshold value. map.put(i,i*10); System.out.println("\nAfter Resize operation:"); printTable(map.table); }else { map.put(i,i*10); } } } private static <K,V>void printTable(Entry<K,V>[] tab){ Entry<K,V> e; int idx=0; while(idx <tab.length ){ e=tab[idx++]; if(e!=null){ do{ System.out.printf("[%d]=%s -> ",idx-1,e.value ); e=e.next; }while(e != null ); System.out.print("null\n"); } else{System.out.printf("[%d]=null\n",idx-1);} } } } ``` 上述程序演示了一个自定义的小型测试用例展示如何打印出每次插入前后的内部哈希桶分布情况以及触发重置的过程。需要注意的是这里简化了一些实现细节方便理解原理即可。 ### 解释: - 在上面的例子中我们手动设定了较小的初始容量与较低负载因子从而加快出现扩容现象以便观察效果; - 方法 `printTable()` 展现现有表格布局状况包括每个索引处是否有节点链接形成链条等情况便于追踪整个变化轨迹。 - 第九次调用put()方法的时候因为已经达到预设门限所以开始扩展空间并将已有内容复制过去调整后的位置。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值