首页 > 其他分享 >Hashmap的扩容机制及扩容后元素迁移

Hashmap的扩容机制及扩容后元素迁移

时间:2022-09-24 18:35:57浏览次数:55  
标签:扩容 Hashmap 容量 next 链表 迁移 null HashMap

一.HashMap基础
HashMap继承了AbstractMap抽象类,实现了Map,Cloneable,Serializable接口。
HashMap的源码属性:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    //初始容量为16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    
    //最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    
    //负载因子默认值
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    //桶上的链表长度大于等于8时,链表转化成红黑树
    static final int TREEIFY_THRESHOLD = 8;
    
    //桶上的红黑树大小小于等于6时,红黑树转化成链表
    static final int UNTREEIFY_THRESHOLD = 6;
    
    //当数组容量大于64时,链表才会转化成红黑树
    static final int MIN_TREEIFY_CAPACITY = 64;
  1. 初始容量:默认为16,在第一次put时生成
  2. 加载因子:默认为0.75
  3. 阈值:阈值=容量*加载因子。默认12,当元素数量超过阈值时便会触发扩容
  4. 最大容量:230 ,当容量超过230 ,容量不再扩大,阈值变为231 -1
  5. java1.8+,桶上的链表长度大于等于8时,链表转化成红黑树
  6. java1.8+,桶上的红黑树大小小于等于6时,红黑树转化成链表
  7. java1.8+,只有当数组容量大于64时,链表才会转化成红黑树

二.何时触发扩容
一般情况下,当元素数量超过阈值时便会触发扩容。每次扩容的容量都是之前容量的2倍。
注意:
HashMap的容量必须为2的n次幂,进行扩容时其容量变为不小于指定容量的2的幂数(即初始化容量过程)。
例如:进行有参初始化时:
new HashMap<>(5)–>初次put时,容量扩为23 =8,数组长度达到阈值6,扩容为16
new HashMap<>(7)–>初次put时,容量扩为23 =8,数组长度达到阈值6,扩容为16
new HashMap<>(13)–>初次put时,容量扩为24 =16,数组长度达到阈值12,扩容为32
new HashMap<>(19)–>初次put时,容量扩为25 =32,数组长度达到阈值24,扩容为64
三.扩容机制
当HashMap决定扩容时,会调用HashMap类中的resize()方法进行扩容。
java1.7下扩容机制
HashMap的底层结构在java1.7版本之前是:数组+单链表。
resize()源码:

   void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {//当原有table长度已经达到了上限,不再扩容。
            threshold = Integer.MAX_VALUE;
            return;
        }
 
        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

源码可见,当原table容量没有到达最大,则会新建一个新的数组,并调用transfer()方法进行元素迁移(见下)。

  • 第一次put时,容量扩容初始化:其容量变为不小于指定容量的2的幂数(默认为16)
  • 不是第一次put时,扩容:新容量=旧容量 * 2 ,新阈值=新容量 * 加载因子

元素迁移
transfer()源码:

 void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;            
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity); 
                e.next = newTable[i];                  
                newTable[i] = e;                  
                e = next;                              
            }
        }
}

在准备好新的数组后,map会遍历数组的每个“桶”,然后遍历桶中的每个Entity,重新计算其hash值(也有可能不计算),找到新数组中的对应位置,以头插法插入新的链表。
注意:
因为是头插法,因此新旧链表的元素位置会发生转置现象。
元素迁移的过程中在多线程情境下有可能会触发死循环(无限进行链表反转),因此HashMap线程不安全。
java1.8+扩容机制
HashMap的底层结构在java1.8版本之后是:数组+单链表/红黑树。
resize()源码:

 final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @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;
                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
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

java1.8与java1.7扩容时容量的计算方法都为扩大为原来容量的二倍。
注意:
因为1.8版本之后HashMap的底层结构为:数组+单链表/红黑树。因此如果某个桶中的链表长度大于等于8了,则会判断当前的hashmap的容量是否大于64,如果小于64,则会进行扩容;如果大于64,则将链表转为红黑树。
元素迁移
java1.8+在扩容时,不需要重新计算元素的hash进行元素迁移。
而是用原先位置key的hash值与旧数组的长度(oldCap)进行"与"操作。

  • 如果结果是0,那么当前元素的桶位置不变。
  • 如果结果为1,那么桶的位置就是原位置+原数组 长度

值得注意的是:为了防止java1.7之前元素迁移头插法在多线程是会造成死循环,java1.8+后使用尾插法
注意:
java1.8 扩容的时候会判断当前的桶的位置有没有链表或者红黑树,如果没有链表或者红黑树,那么当前元素还是和JDK1.7中的求法一样,求新的桶的位置。如果有链表,那么链表的元素会按照上述方法求新的桶的位置。如果是红黑树,则会调用split()方法,将红黑树切分为两个链表,之后进行扩容操作。

标签:扩容,Hashmap,容量,next,链表,迁移,null,HashMap
From: https://www.cnblogs.com/jelly12345/p/16726184.html

相关文章

  • linux微服务迁移
    shell脚本--拉取代码包背景:jenkins上线项目,更新项目包#!/bin/bash#====================================================#Description:UpdatethepackageforP......
  • win11 wsl2-ubuntu20.04从C盘迁移到D盘
     核心步骤:1)导出到D盘2)从C盘中注销原始的ubuntu系统3)从D盘中重新导入 wsl--exportUbuntu-20.04d://wslubuntu2004//ubuntu-20.04.tarwsl--unregisterUbuntu......
  • 如何将 Netlify GoTrue 用户迁移到 Appwrite
    如何将NetlifyGoTrue用户迁移到AppwriteMigratingNetlifyGoTrueUsers借助Appwrite1.0,我们很高兴地宣布您可以将不同平台的用户导入Appwrite。这些平台之一是......
  • MariaDB数据库迁移目录
     1、确定mysql数据库文件存放目录1showvariableslike'%dir%';2、停止mysql数据库服务:1sudo/etc/init.d/mariadbstop3、......
  • 中仑网络全站 Dubbo 2 迁移 Dubbo 3 总结
    简介: 中仑网络在2022年完成了服务框架从Dubbo2到Dubbo3的全站升级,深度使用了应用级服务发现、Kubernetes原生服务部署、服务治理等核心能力。来自中仑网络的技术......
  • 将带有 ExpressJS 后端的 React 应用程序从 Heroku 迁移到 Netlify
    将带有ExpressJS后端的React应用程序从Heroku迁移到NetlifyPhotoby克里斯布里格斯on不飞溅Heroku将开始对HerokuDynos收费,这是运行节点服务器所需的资......
  • 使用V2V功能将VMware平台虚拟机迁移至深信服Sangfor平台
     1、进入虚拟机备份系统  2、选择【虚拟机保护】——【恢复】,新建恢复任务,选择需要进行跨平台恢复与迁移的源虚拟化平台【VMwarevSphere】,勾选需要恢复的备份点,点......
  • TD8.0迁移到QC9.0
    服务器A上装有TD8.0服务器B上装有QC9.0将TD8.0的项目迁移到QC9.0上,QC9.0所用数据库为TD8.0原数据库。(数据库为SQLServer)1、在机器B上安装QC9.0,安装过程中数据库连接为TD......
  • KVM 虚拟机硬盘分区扩容
    分区情况如下,扩容sda1注意,sda1必须占满硬盘后面的所有空间(最后34个扇区,是GPT分区的备用区块,不能动)$lsblkNAMEMAJ:MINRMSIZEROTYPEMOUNTPOINTsda......
  • TD项目数据迁移
    TestDirect(以下简称TD)它是]MercuryInteractive公司推出的基于WEB浏览器环境下的测试管理工具。通过TD的流程控制可以规范软件企业的测试流程、改善测试质量、减轻测试人......