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)
总结:此文是作者原创,有任何不足,请指出修正,大家相互学习, 谢谢