HashMap-1.8 之初始化容量与参数设置(源码分析)

 

1. 初始化容量源码分析

1.1 第一步:创建初始化容量代码

// 初始化容量
int initCapacity = 25;
HashMap<String, Object> hashMap = new HashMap<>(initCapacity);

其中initCapacity是需要初始化的容量, 跟进源码进入HashMap的构造函数

1.2 第二步:HashMap(int initialCapacity)初始化容量的构造函数

/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and the default load factor (0.75).
 *
 * @param initialCapacity the initial capacity.
 * @throws IllegalArgumentException if the initial capacity is negative.
 */
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

当进入到这个函数时,他会自动调用HashMap(int initialCapacity, float loadFactor)构造函数,其中initialCapacity是我们传进来的初始化容量 initialCapacity = 25,DEFAULT_LOAD_FACTOR是扩容时的加载因子 0.75f

1.3 第三步: HashMap(int initialCapacity, float loadFactor)构造函数

/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and load factor.
 *
 * @param  initialCapacity the initial capacity
 * @param  loadFactor      the load factor
 * @throws IllegalArgumentException if the initial capacity is negative
 *         or the load factor is nonpositive
 */
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;
    this.threshold = tableSizeFor(initialCapacity);
}

先来了解一下几个变量的含义:

size

size表示HashMap中存放KV的数量(为链表和树中的KV的总和)。

capacity

capacity译为容量。capacity就是指HashMap中桶的数量。默认值为16。一般第一次扩容时会扩容到64,之后好像是2倍。总之,容量都是2的幂。

loadFactor

loadFactor译为装载因子。装载因子用来衡量HashMap满的程度。loadFactor的默认值为0.75f。计算HashMap的实时装载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity。

threshold

threshold表示当HashMap的size大于threshold时会执行resize操作。 

threshold=capacity*loadFactor

MAXIMUM_CAPACITY

HashMap的最大容量(MAXIMUM_CAPACITY = 1 <<< 32)

在这里initialCapacity = 25,loadFactor = 0.75f,MAXIMUM_CAPACITY是HashMap的最大容量(MAXIMUM_CAPACITY = 1 <<< 32),根据我们初始化容量initialCapacity = 25的调节,会直接执行 this.threshold = tableSizeFor(initialCapacity), 这个函数其实就是对HashMap进行容量的初始化操作。

1.4 第四步:tableSizeFor(int cap)扩容

/**
 * Returns a power of two size for the given target capacity.
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;  // n = 24
    n |= n >>> 1;     // n = 24 | (24 >>> 1) = 28
    n |= n >>> 2;     // n = 28 | (28 >>> 2) = 31
    n |= n >>> 4;     // n = 31 | (31 >>> 4) = 31
    n |= n >>> 8;     // n = 31 | (31 >>> 8) = 31
    n |= n >>> 16;    // n = 31 | (31 >>> 16) = 31
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

传进来的参数cap = 25,经过一系列算法之后, n 的最终值为 23 ,经过两次三元表达式运算, 返回的是 n + 1 = 32。当程序执行完毕,HashMap初始化的容量就为32。

2. 初始化容量的问题

但是这里就有一个问题:我指定的初始化容量是25,HashMap初始化后的容量是32。根据1.8中的HashMap扩容原理可知,当HashMap存储数据后的size >= 0.75f  * 32 时, hashMap底层会再次扩容至2的n次幂, 也就是HashMap的容量扩容到 64。而HashMap带着数据扩容是不安全的,所以要尽量避免。这里的初始化容量是明显  25 >= 0.75f * 32 = 24 的,解决方案如下:

  • 在初始化容量时,尽可能的避免再次扩容。
  • 如果知道具体的容量,那么:
int 扩容的容量 = (int)(具体的容量 / 0.75f)

为什么要这么算呢?我们说到当传进去的初始化容量为25时,在存储数据时,size达到24,HashMap就会扩容一次,为了避免这种不安全的带数据扩容操作,当我们对应当扩容的容量进行算法操作后 initCapacity = 25 /0.75f = 33(取整),如果初始化容量的值在12 - 23之间, 算出来的initCapacity的值绘制16 - 32之间,经过底层的算法之后,扩容后的容量只能是32。

// 需要初始化的容量
int size = 25;
// 实际需要初始化的真实容量
int initCapacity = (int) (size / 0.75f);
HashMap<String, Object> hashMap = new HashMap<>(initCapacity);

这时候的initCapacity = 33,经过底层的算法之后,扩容后的容量是64。

  • 如果不知道具体的容量,在这里建议给定他可能的容量, 参照下面这个公式计算
int 扩容的容量 = (int)(具体的容量 / 0.75f)