美文网首页
HashMap的构造方法及第一次put时的扩容操作

HashMap的构造方法及第一次put时的扩容操作

作者: 袁小象 | 来源:发表于2019-03-05 11:05 被阅读0次

属性

HashMap中有一些重要属性如下:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient int size;
int threshold;
final float loadFactor;
transient Node<K,V>[] table;

构造器

自定义了四个构造器(包括无参数的默认构造器):

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
      throw new IllegalArgumentException("Illegal initial capacity: " +
                                         initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
      initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
      throw new IllegalArgumentException("Illegal load factor: " +
                                         loadFactor);
    this.loadFactor = loadFactor;
    //tableSizeFor的作用是求得不小于initialCapacity的最小2的幂次方值,
    //比如initialCapacity是15,求得16=2^4;
    this.threshold = tableSizeFor(initialCapacity); 
}

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

//暂时不讨论该构造器
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

一、HashMap(int initialCapacity, float loadFactor)

该构造器首先确保参数的有效性,且initialCapacity不能大于MAXIMUM_CAPACITY(2 ^ 30),然后初始化了loadFactorthreshold,注意到并没有去开辟table数组占用的内存,那什么时候开辟呢?答案是第一次put的时候。而此时的阈值为什么要是一个2的幂次方值呢?显然threashold应该和capacityloadFactor相关,但此处直接将tableSizeFor(initialCapacity)的值赋值给threashold,是不是不合理呢?下面会讲。

二、HashMap(int initialCapacity)

该构造器和调用上一个构造器,只是将loadFactor赋值为DEFAULT_LOAD_FACTOR(0.75),也没有开辟桶数组的空间。

三、HashMap( )

该构造器只是将loadFactor赋值为DEFAULT_LOAD_FACTOR(0.75),注意到调用这个构造器后,threshold为0,该构造器也没有开辟桶数组的空间。

下面这个表对后面的分析会有用处:

构造器 this.loadFactor this. threshold this.table this.size
HashMap(int initialCapacity, float loadFactor) loadFactor tableSizeFor(initialCapacity) null 0
HashMap(int initialCapacity) DEFAULT_LOAD_FACTOR tableSizeFor(initialCapacity) null 0
HashMap( ) DEFAULT_LOAD_FACTOR 0 null 0

四、第一次put操作

创建了一个HashMap之后,紧接着就会向map中添加键值对,map.put(key, value)
第一次put时会开辟数组空间。看代码:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
      Node<K,V>[] tab; Node<K,V> p; int n, i;
      if ((tab = table) == null || (n = tab.length) == 0) 
            //第一次调用put操作时,table都是null,所以必走这一步
            n = (tab = resize()).length;  //resize()的作用就是开辟数组空间,此时tab就是新开辟数组的引用
      //开辟出新数组之后,计算当前key应该放在哪个slot中
      if ((p = tab[i = (n - 1) & hash]) == null)  //i = (n - 1) & hash,计算索引
            tab[i] = newNode(hash, key, value, null);
      else {
            //第一次调用put的时候,下面代码都不会执行
            //...
      }
      ++modCount;
      //put完之后,size值会加1,如果此时大于阈值,将会扩容。
      if (++size > threshold)
            resize();
      return null;
}

可以看出put操作的主要流程:

  • 分配数组空间
  • 插入键值对
  • ++size,++modCount
    下面主要看看开辟数组空间的逻辑:
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table; // now the table is null
        int oldCap = (oldTab == null) ? 0 : oldTab.length;  // so oldCap == 0
        /*
         * 1、若调用的是带有参数的构造器,oldThr = tableSizeFor(initialCapacity)  > 0,
         * 2、若调用的是无参数的构造器,oldThr == 0
        */
        int oldThr = threshold; 
        int newCap, newThr = 0;
        if (oldCap > 0) {
            //下面代码不会调用,因为oldCap == 0
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            //若调用的是带有参数的构造器,此时计算的桶数组大小就是 tableSizeFor(initialCapacity) > 0
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            //若调用的是无参数的构造器,newCap == 16, newThr == (int) 16 * 0.75
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            //到这里如果newThr还没有被赋值,就执行下面代码
            //为什么到这里newThr依然为0呢?如果调用带有参数的构造器,那么threashold是大于0的,那么
            //就执行上面的if(oldThr > 0),将oldThr值赋给newCap,此时newThr依然为0, 而原来的threshold也就没有了用处
            //这说明之前将一个2的幂次方值赋值给threshold,只是为了赋值给桶数组的默认容量值(newCap = oldThr),赋值完之后,
            //threashold就需要重新计算了,下面就是计算过程:
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //覆盖threshold
        threshold = newThr;
        //开辟空间在这里
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            //这里面的代码不会调用,因为oldTab == null。
        }
        //返回桶数组的引用
        return newTab;
}

总结

1、调用HashMap构造器的时候,并不会开辟桶数组的空间。ArrayList的构造器在指定大小的时候会分配空间,而无参数的构造器则会分配一个长度为0的数组。

2、调用initialCapacity参数的构造器后,第一次put时的数组空间大小是不小于initialCapacity的最小2的幂次方值。

3、调用无参的构造方法之后,第一次puttable数组的空间大小是16,threshold是16*0.75=12。

3、先put,然后比较size和threshold,进行扩容,扩容到两倍。

4、参数含义:

参数 含义
size map中的key-value对的数量
table.length 即HashMap的容量,capacity
threshold 当size>threshold的时候,进行扩容,一般情况下threshold = capacity * loadFactor
loadFactor 负载因子,表明容器的利用率,可以大于1,一般不需要修改,默认值即可。

参考

1、https://www.zhihu.com/question/47028049
2、https://zhuanlan.zhihu.com/p/30360734

相关文章

网友评论

      本文标题:HashMap的构造方法及第一次put时的扩容操作

      本文链接:https://www.haomeiwen.com/subject/ylknuqtx.html