本来想写一篇关于HashMap完整的源码分析的,结果我发现整理了一下东西是真的多,而且也怕误人子弟,那就分析一下为什么阿里Java开发手册里为要指定HashMap的容量吧。
让我们带着问题进入:
其他的关于HashMap可以说的东西太多了,今天就根据阿里开发手册做一个探讨。
首先贴出阿里开发手册1.4关于HashMap的部分:
【推荐】集合初始化时,指定集合初始值大小。 说明:HashMap 使用 HashMap(int initialCapacity) 初始化。 正例:initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即loader factor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。 反例:HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素不断增加,容 量 7 次被迫扩大,resize 需要重建 hash 表,严重影响性能。
注:要想更快的理解如下代码最好新复习一下Key的HashCode生成规则,以及put时候如何将Key转换成HashMap后存入transient Node
首先贴出需要了解的源码:
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 如果旧数组容量大于0
if (oldCap > 0) {
// 如果容量大于容器最大值
if (oldCap >= MAXIMUM_CAPACITY) {
// 阀值设为int最大值
threshold = Integer.MAX_VALUE;
// 返回旧数组,不再扩充
return oldTab;
}// 如果旧的容量*2 小于最大容量并且旧的容量大于等于默认容量
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 新的阀值也再旧的阀值基础上*2
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
// 新容量等于旧阀值
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 如果容量是0,阀值也是0,认为这是一个新的数组,使用默认的容量16和默认的阀值12
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新的阀值是0,重新计算阀值
if (newThr == 0) {
// 使用新的容量 * 负载因子(0.75)
float ft = (float)newCap * loadFactor;
// 如果新的容量小于最大容量 且 阀值小于最大 则新阀值等于刚刚计算的阀值,否则新阀值为 int 最大值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 将新阀值赋值给当前对象的阀值。
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//以上源码是为了确定HashMap的大小以及可存储多少元素
//==============================================================================
//以下代码是将旧数据放入到扩容的新数组中
// 创建一个Node 数组,容量是新数组的容量(新容量要么是旧的容量,要么是旧容量*2,要么是16)
Node[] newTab = (Node[])new Node[newCap];
// 将新数组赋值给当前对象的数组属性
table = newTab;
// 如果旧的数组不是null
if (oldTab != null) {
// 循环旧数组
for (int j = 0; j < oldCap; ++j) {
// 定义一个节点
Node e;
// 如果旧数组对应下标的值不为空
if ((e = oldTab[j]) != null) {
// 设置为空
oldTab[j] = null;
// 如果旧数组没有链表
if (e.next == null)
// 将该值散列到新数组中
newTab[e.hash & (newCap - 1)] = e;
// 如果该节点是树
else if (e instanceof TreeNode)
// 调用红黑树 的split 方法,传入当前对象,新数组,当前下标,旧数组的容量,目的是将树的数据重新散列到数组中
((TreeNode)e).split(this, newTab, j, oldCap);
else { // 如果既不是树,next 节点也不为空,则是链表,注意,这里将优化链表重新散列
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
// 第一次进来时链头赋值
if (loTail == null)
loHead = e;
else
// 给链尾赋值
loTail.next = e;
// 重置变量
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
// 销毁实例,等待GC回收
loTail.next = null;
// 置入bucket中
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
总结:HashMap在扩容的时候resize 需要重建 hash 表,所以才会影响性能。
1.为什么要使用构造函数指定HashMap的容量
避免多次扩容
2.如果不指定会对效率造成多大的影响
以放置 1024 个元素为例,容量7次扩容,其中不光是七次重新计算HashCode,如果HashCode碰撞较多,还会涉及链表(链表中数据>=8,并且HashMap容量<64会进行重新散列,如果HashMap容量>64就会进行红黑树的转换),以及红黑树的转换等。扩容中我比较喜欢的地方在于重新重建hash 表后,原来链表中以及树种的内容可能就不会因为冲突导致以链表或者树的形式存在!比较欣慰。
扩展话题:如果需要制定HashMap的容量那么多少为好呢?
源码:
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
这个方法会将我们如数的值寻找最近的2的幂次方,如输入10则会转换为16
我们知道了,无论我们如何设置初始容量,HashMap的tableSizeFor(int cap) 都会将我们改成2的幂次方,也就是说,HashMap 的容量百分之百是 2的幂次方,因为HashMap 太依赖他了。但是,请注意:如果我们预计插入7条数据,那么我们写入7,HashMap 会设置为 8,虽然是2的幂次方,但是,请注意,当我们放入第7条数据的时候,就会引起扩容,造成性能损失,所以,知晓了原理,我们以后在设置容量的时候还是自己算一下,比如放7条数据,我们还是都是设置成16,这样就不会扩容了。
计算公式:
这里就要说到手册里提到的 “注意负载因子(即loader factor)默认为 0.75”
假如我们要插入7条数据,tableSizeFor(int cap)会将我们输入的7运算成8。
我们使用 8* 0.75(负载因子)=6 也就是说最大阈值为6条
当我们插入第七条的时候它就扩容了,所以我们最好在指定容量的时候多预算一些。
另:不光是HashMap需要指定大小,其他数据结构在知晓其存储的数量时也应指定。
摘自《阿里开发手册》
【推荐】任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存。