前言
阅读前请先看看如下几个问题,如果有不清楚的地方看完本文会有收获
关于扩容相关原理与一些巧妙的二进制算法分析参考HashMap巧妙的扩容算法
HashMap(int initialCapacity)
在JDK的HashMap中存在无参构造函数和有参构造函数,有参构造函数中又存在带有指定容量和加载因子的构造方法和只带有指定容量的构造方法,本文主要分析下 HashMap(int initialCapacity) 构造方法
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
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);
}
该方法逻辑比较简单首先校验下容量有没有超多最大值,没有的话设置下加载因子和扩容阈值
算法分析
生成扩容阈值的算法如下,该算法的非常巧妙的用位运算生成大于等于指定容量的最小的2的幂次值例如如果给定一个数为14那么算出大于14的2的最小指数幂等于16
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的整数次幂对应的二进制数有效位为1且有效位后所有位为0,如下图所示
- 首先解释下为什么先做cap-1操作?当cap已经为2的整数次幂时通过该方法计算会将原来的cap变为2*cap,其实就是为了处理特殊的数据
- 在解释下为什么每次无符号移位操作是1,2,4,8,16?在java中int类型为32位,1+2+4+8+16正好是移动32位,移动32位后的得到的结果是将最高有效位及以后所有位变0位1
假设cap=14,cap-1 =13 其二进制位1101经过下图的变化后发现在第一次位运算后所有有效位都为1111,因此在剩下的四次位运算后最终结果就是1111,最后将1111+1=10000得到十进制数位16。
此时虽然设置了hashmap的threshold但是并没有真正设置hashmap的容量大小,真正容量大小是在向hashmap中插入第一个元素时进行设置
HashMap 内存分配时机
经过构造函数new的Hashmap对象并未真正分配内存空间,这其实是一种优化因为在构造方法就分配内存空间后很久才进行插入操作那么这部分内存其实是浪费的。因此在put操作时会进行判断如果第一次插入则通过resize方法进行进行分配空间。第一次插入时分配的空间大小即为tablesizefor方法返回大小
这种分配方式满足程序局部性原理,在写代码时可以借鉴
- put防范插入数据前判断当前hashMap中table是否初始化
- 未初始化进行resize操作,其实hashmap的初始内存分配跟扩容都是通过resize方法来完成
- resize方法内部通过各种参数判断来执行初始化逻辑跟扩容逻辑
内存分配逻辑-resize
本文只截取部分resize方法分析,剩余扩容逻辑会在新文章中进行讲解。
- 首先判断table 是否为空,因为初始化阶段table=null所以oldCap=0
- 由于在hashmap中带参数的构造方法通过tableSizeFor方法设置了threshold因此table数据的容量为oldThr
- 如果使用无参数构造方法newCap&newThr均使用默认值
- 最后创建数据并指向table
总结
HashMap使用非常巧妙且高效的算法获取了大于等于给定容量的最小2的整数幂,所以在根据hashcode计算元素所属hash表的位置时通过hashcode与(hash表长度-1)进行取模快速定位。
在首次插入进行内存分配机制极大的避免了内存浪费而且满足了局部性原理同样值得学习