• 由浅入深解析HashMap系列二---扩容


    由浅入深解析HashMap系列一---HashMap简单实现 增、删、查。

       前面简单的实现了hashmap的增删查功能,这一章节主要是介绍扩容(不考虑冲突),当数组元素达到一定阈值时,需要扩容,扩容之后,需要对原来的数组中的元素进行再次hash。在开始之前先介绍几个概念:

    基本概念

       初始容量:DEFAULT_INITIAL_CAPACITY = 1 << 4。在JDK1.8中,默认的容量为16。在初始化HashMap时,并不会按照这个初始容量初始化数组,而是在第一个put元素时,才初始化数组。

       加载因子:DEFAULT_LOAD_FACTOR = 0.75f ,哈希表在其容量自动增加之前可以达到多满的一种尺度。

       阈值:threshold,等于capacity * load factor。比如说:初始时,初始容量为16,加载因子为0.75,则阈值为12。当哈希表中的节点个数为12个时,就需要扩容。也许按照我们的思维习惯,当节点个数达到16时,才扩容。加载因子过低,如果扩容之后,不再有数据增加,增加了空间开销;加载因子过高虽然减少了空间开销,但同时也增加了查询成本,所以需要对其有这个折中。

    扩容操作

      具体步骤如下:

    1)获取新的容量和阈值。根据old capacity,经过扩容算法,获取new capacity和threshold。扩容算法:新的容量=原来的容量*2;新的阈值=原来的阈值*2;初始容量=16;初始阈值=初始容量*加载因子=16*0.75。
    2)对原有数组元素重新分布。前一章节提到过,下标index的获取与数组长度也就是capacity有关,通过上面获取的新的capacity,重新计算节点下标,也就是对原有哈希表中的元素再散列到新的哈希表中。
    注意:当原有容量已经达到最大容量时,就不允许再扩容,而是返回初始数组。

     1  /**
     2      * 1)获取新的容量和阈值。
     3      * 2)对原有数组进行再散列。
     4      * 注意:当原有容量已经达到最大容量时,就不允许再扩容,而是返回初始数组。
     5      * 新的容量=原来的容量*2;新的阈值=原来的阈值*2;初始容量=16;初始阈值=初始容量*加载因子=16*0.75
     6      * @return
     7      */
     8     final Node<K,V>[] resize() {
     9         Node<K,V>[] oldTab = table;//oldTab保存扩容前的数组
    10         int oldCap = (oldTab == null) ? 0 : oldTab.length;//如果扩容前数组为Null则oldCap为0,否则为扩容前数组长度
    11       //旧的扩容大小,一般等于容量*loadfactor。当数组大小达到阈值threshold时,就扩容。注意并不是数组满了才扩容
    12         int oldThr = threshold; 
    13         //初始化新的容量和阈值
    14         int newCap, newThr = 0;
    15         //旧的容量大于0
    16         if (oldCap > 0) {
    17             //这样设计应该是不允许扩容了,已经达到极限了。
    18             if (oldCap >= MAXIMUM_CAPACITY) {//旧的容量达到最大容量时,阈值设置为最大整数。返回原来的数组。
    19                 threshold = Integer.MAX_VALUE;
    20                 return oldTab;
    21             }
    22             //新的容量=旧的容量*2。新的阈值=旧的阈值*2
    23             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
    24                      oldCap >= DEFAULT_INITIAL_CAPACITY)
    25                 newThr = oldThr << 1; // double threshold
    26         }//旧的容量小于等于0时,若旧的阈值大于0,新的阈值旧等于旧的阈值。
    27         else if (oldThr > 0) // initial capacity was placed in threshold
    28             newCap = oldThr;
    29         //旧的容量小于等于0,旧的阈值小于等于0,新的容量和阈值都等于默认值。这一般是初次调用hashmap时触发
    30         //阈值=加载因子*初始容量=0.75*16=12。也就是当初次添加元素达到12个时,就扩容。
    31         else {               // zero initial threshold signifies using defaults
    32             newCap = DEFAULT_INITIAL_CAPACITY;
    33             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    34         }
    35         //对阈值进行设计,临界条件的判断,不明白到这一步阈值为啥还会为0
    36         if (newThr == 0) {
    37             float ft = (float)newCap * loadFactor;
    38             newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
    39                       (int)ft : Integer.MAX_VALUE);
    40         }
    41         //设置阈值
    42         threshold = newThr; 
    43         //初始化新的数组,对原有的数组进行再散列
    44             Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    45         table = newTab;
    46         if (oldTab != null) {
    47             for (int j = 0; j < oldCap; ++j) {
    48                 Node<K,V> e;
    49                 if ((e = oldTab[j]) != null) {
    50                     oldTab[j] = null;
    51                     if (e.next == null)
    52                         newTab[e.hash & (newCap - 1)] = e;
    53                 }
    54             }
    55         }
    56         return newTab;
    57     }
    View Code

    put操作

    put操作在上一节的基础上会添加一些功能,如下:

    1)判断数组(Node<K,V>[] table)是否为null或者长度为0。上节为了简便在初始化HashMap时,构建一个长度为100的数组,而在这一章节中,初始化数组的操作延迟到第一次添加元素的时候。故在put操作时,要判断数组的长度是不是为0或者为Null。

    2)当数组中元素的个数达到给定阈值时,调用resize函数扩容。

     1 /**
     2      * 添加。
     3      * 考虑扩容,但是不考虑冲突
     4      * @param key
     5      * @param value
     6      * @return
     7      */
     8     public V put(K key, V value)
     9     {
    10         //创建新节点
    11         Node<K,V>[] tab;Node<K,V> p; int n, i;
    12         //一般初次才会走这个步骤
    13         if ((tab = table) == null || (n = tab.length) == 0)
    14             n = (tab = resize()).length;
    15         //对key值进行hash
    16         int hash=hash(key);
    17         //目前没有考虑hash冲突
    18         table[i = (n - 1) & hash]=newNode(hash,key,value,null);
    19         if(++size>threshold)//当达到阈值时,需要扩容
    20             resize();
    21         //返回原来的值
    22         return value;
    23         
    24     }
    View Code

     

    只有在添加元素的时候,才需要考虑resize,在删除、查询时,不需要考虑,所以remove、get函数和上一章节是一样的。

       个人观点:因resize导致重新hash,节点重新散列,故初始容量和加载因子的设置还是很重要的。

     

     

  • 相关阅读:
    书单
    x&(x1)表达式的意义
    约瑟夫环,杀人游戏(静态循环链表实现)
    我的第一个动态规划程序(试图用递归求斐波拉契数)
    NYOJ 2题 括号配对问题
    为什么 C++不叫作++C? o(∩_∩)o
    文字常量区,字符串常量
    括号匹配(栈实现)
    Mybatis的逆向工程(generator)
    Mybatis学习一(介绍/举例/优化)
  • 原文地址:https://www.cnblogs.com/icbcfang/p/5418983.html
Copyright © 2020-2023  润新知