HashMap的扩容机制 - 极悦
首页 课程 师资 教程 报名

HashMap的扩容机制

  • 2022-05-17 10:14:56
  • 2308次 极悦

先说扩容原理

在jdk1.7中,扩容需要满足以下两个条件:

1.存储新值时已有元素的个数必须大于等于阈值

2.当前存储数据发生在新值存储hash时碰撞

在jdk1.8中,扩容只需要满足一个条件:当前存储新值时已有元素的个数大于等于阈值(注意不替换已有元素)(已有元素等于阈值,下一个存储必然会触发扩容机制)或者将数据保存到链表中。这时候如果数据大于8,总数小于64,就会发生扩容。注意:

(1)扩展必须在放置新值的时候,新值不在替换之前位置的情况下

(2)扩展发生后,会判断存储的对象个数。如果大于阈值,将执行扩展。

(3)第一次put的时候,先触发扩容(实例化的HashMap的内部数组默认为null,即不实例化。第一次调用put方法时,会先启动初始化和扩展,长度为16.),然后保存数据,然后判断是否需要扩展;如果不是第一次,则不会再次初始化。直接保存数据,然后判断是否需要扩容;

HashMap的容量有一个上限,必须小于1<<30,即1073741824。如果容量超过这个数,就不再增加,阈值设置为Integer.MAX_VALUE([公式] ,即永远不会超过阈值)。

源码:jdk7中new Hashmap()时初始化对象,而jdk8中new Hashmap()并没有初始化对象,而是在put()方法中通过判断对象是否为空,如果为Null初始化通过调用 resize() 对象。

	 public V put ( K key , V value )  { 
       	 return  putVal ( hash ( key ) , key , value ,  false ,  true ) ; 
    	} 
/**
     * 实现 Map.put 及相关方法
     *
     * @param 哈希键值计算传递下标
     * @参数键
     * @参数值
     * @param onlyIfAbsent true 只在值为空时存储数据,false 存储数据
     * @param 驱逐
     * @return 返回覆盖的值,如果没有覆盖则返回 null
     */ 
    final V putVal ( int hash , K key , V value ,  boolean onlyIfAbsent ,                    boolean evict )  { 
        //声明入口数组 object tab[]: current Entry[] object  
        Node < K , V > [ ] tab ; 
        //声明入口对象p:这里表示存储的单个节点 
        Node < K , V > p ;
        //n:为当前Entry对象的长度//i:为下标
        int n , i为当前存放对象节点的位置;
        /**
         * 过程判断
         * 1.如果当前Node数组(tab)为空,直接创建(通过resize()创建),创建后设置当前长度为n
         * 2.如果要存储对象的Node节点为空,直接在对象存储位置新建Node,直接存储值
         * 3.存储的Node数组不为空,存储的下标节点Node不为空(Node节点为链表的第一个节点)
         * 1) 比较存储在链表第一个节点的对象和当前对象是否为同一个对象,如果是则覆盖并返回原值
         * 2) 如果不分两种情况
         * (1)存储节点为红黑树节点结构,调用putTreeVal()方法直接插入数据
         * (2)如果不是红黑树,则表示为链表,遍历
         * A.如果存储的链表的下一个位置为空,先直接存储该值,保存后再检查当前存储的位置是否大于链表的第8位
         * 一种。如果大于,则调用treeifyBin方法判断是否对链表进行扩展或转换为红黑树(大于8且总数据量大于64,则转红黑,否则为数组将扩大)
         * b。当前保存的位置链表长度不大于8,则保存成功,终端循环操作。
         * B. 如果链表中存储的下一个位置有值,且该值与存储的对象“相同”,则直接覆盖,返回原值
         * 以上两种情况AB执行后,判断返回的原始对象是否为空,如果不是,则返回原始对象的原始值
         * 上述123三种情况中,如果原值没有被覆盖,则表示新增了新存储的数据。数据存储后,size+1,然后判断当前数据量是否大于阈值。
         * 如果大于阈值,则扩容。
         * 
        / if  ( ( tab = table )  == null ||  ( n = tab.length ) == 0 )  
            n = ( tab = resize ( ) ) 。 _ 长度;if ( ( p = tab [ i = ( n - 1 ) & hash ] ) == null    
             ) 
            tab [ i ]  =  newNode ( hash , key , value , null ) ; 
        否则 { 
            节点< K , V > e ; ķķ ; 
            if  ( p . hash == hash && 
                    ( ( k = p . key )  == key ||  ( key != null&&键等于( . k ) ) ) )  
                e = p ; 
            else  if  ( p instanceof  TreeNode ) 
                //直接将数据存入红黑树 
                e =  ( ( TreeNode < K , V > ) p ) . putTreeVal (这,选项卡,哈希,键,值); 
            else  { 
                for  ( int binCount =  0 ;  ;  ++ ; binCount )  { 
                    if  ( ( e = p . next )  == null )  {  
                        p . next =  newNode (哈希,键,值, null ) ; 
                        如果 ( binCount >= TREEIFY_THRESHOLD - 1 )  //-1 for 1st 
                            treeifyBin ( tab , hash ) //该方法判断是扩展还是需要将链表转换为红黑树break ;}                                          
                    if  ( e . Hash == hash && 
                            ( ( k = e . Key )  == key ||  ( key ! = null && key . Equals ( k ) ) ) )
                        ;  
                    p = e ; 
                } 
            } 
            if  ( e != null )  {  //key  
                V oldValue = e的现有映射。价值;if ( ! onlyIfAbsent || oldValue == null )  
                    e . 价值=价值;afterNodeAccess ( e ) ; 返回旧值;} }     
        ++ modCount ; 
        //如果不是替换数据保存,而是新位置保存后,地图大小加1,然后判断容量是否超过阈值,如果超过则扩容
        if  ( ++ size > threshold )
            调整大小( ) ; 
        afterNodeInsertion (驱逐) ; 
        返回空值;
    }

treeifyBin() 方法判断是否将当前链表展开或转换为红黑树

/**
     * 替换给定哈希索引处 bin 中的所有链接节点,除非
     * 表太小,在这种情况下调整大小。
     * 从链表中指定哈希位置的节点开始,全部替换为红黑树结构。
     * 除非整个数组对象(Map集合)的数据量非常小(小于64),这种情况下Map是通过resize()来扩展的,而不是把链表转化为红黑树。
     */ 
    final  void  treeifyBin ( HashMap . Node < K , V > [ ] tab ,  int hash )  { 
        int n , index ; 哈希映射。节点< K , V > e ; 
        //如果Map为空或者当前存储数据n(可以理解为map的size())个数小于64,然后进行扩容
        if  ( tab == null||  ( n = tab.Length ) < MIN_TREEIFY_CAPACITY )调整大小( ) ; _  //如果size()大于64,则将存储值的链表转换为红黑树else if ( ( e = tab [ index = ( n - 1 ) & hash ] ) != null ) { 
            哈希映射。树节点< K ,                    
               V > hd = null , tl = null ; 
            做 { 
                哈希映射。TreeNode < K , V > p =  replacementTreeNode ( e , null ) ; 
                如果 ( tl == null )  
                    hd = p ; 
                否则 {  
                    p 。上一页= tl ;  
                    tl. 下一个= p ; 
                }  
                tl = p ; 
            }  while  ( ( e = e . next )  != null ) ; 
            如果 ((标签[索引]  =高清) !=空) 
                高清。树化(选项卡);
        } 
    }

子问题一:哈希算法

Java 源代码执行此操作:

先根据key计算hashCode值,然后对数组长度减一做AND运算。为什么当 hashmap 数组的初始大小是 2 的幂时,hashmap 效率最高?你可以看到第七个问题。因此,当数组长度为2的n次幂时,不同key计算的索引相同的概率较小,则数据在数组上分布更均匀,查询效率更高.

HashMap中默认的数组大小为16,因为16是2的整数次幂,在数据量较小的情况下可以更好的减少key之间的冲突。存储大容量时,最好预先指定HashMap的大小为2的整数次幂。如果不指定,HashMap的构造方法也会初始化为大于和最接近指定的2次幂价值。

子问题2:HashMap的resize

HashMap展开时:必须重新计算原数组中的数组,放入新数组中。这是调整大小。那么我们什么时候应该扩张呢?当 HashMap 中的元素个数超过数组大小 * loadFactor(加载因子)时,数组会被扩展。loadFactor 的默认值为 0.75。

所以一开始,在指定HashMap的大小时,我们把它设置为最接近我们预期的数/0.75,得到最接近并大于他的数(这个数是2的指数倍数),所以我们同时考虑 & 的问题,也避免了调整大小的问题。

如果大家对此比较感兴趣,想了解更多相关知识,可以关注一下极悦的HashMap扩容机制解读,里面有更丰富的知识等着大家去学习,希望对大家能够有所帮助哦。

选你想看

你适合学Java吗?4大专业测评方法

代码逻辑 吸收能力 技术学习能力 综合素质

先测评确定适合在学习

在线申请免费测试名额
价值1998元实验班免费学
姓名
手机
提交