提到HashMap,文章可谓是不可胜数,“详解HashMap”啊,“HashMap源码解析”啊,“细说HashMap啊”,“胡说HashMap”啊....数组
测试以前,先回顾一下 Hashmap 都有哪些重要的元素? 首当其冲重要的就是三个成员变量 capacity、loadFactor、threshold,其中 capacity 和 loadFactor 是能够从构造函数中传入的,通俗的来捋一波:bash
capacity : HashMap 中桶的数量, 总容量默认值是 16 。须要注意的是初始容量必须是 2 的幂次方。
关于这个属性,还让笔者想起昨天在脉脉上看的小段子,一个员工吐槽:"在老板的代码里发现了new HashMap<> (3)
这段代码"。刚刚笔者也说到初始容量必须是 2 的幂次方,可是写成 3,也不会报错,强大的 hashmap 都帮咱们考虑好了,内部用了 tableSizeFor 这个方法,帮咱们转换了一下:数据结构
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;
}
intput 3 | 4 | 5 | 8 ......
output 4 | 4 | 8 | 8
复制代码
capacity 就是传入的 3,hashmap 拿到 3 并不会直接使用,通过 tableSizeFor 运算后会获得 2 的幂次方 4,全部你这里传 3 仍是传 4,hashmap 都会优化为 4,脉脉上吐槽的员工估计是想嘲笑一下老板连基础知识都不扎实吧,我以为老板是 Capacity 用不用减一记混了可能比较大吧,也不影响使用。言归正传..dom
loadFactor:负载因子,默认 0.75。也就是threshold是根据 capacity * loadfactor 算出来的,HashMap 会根据 threshold 的数值,来决定何时调整空间大小。 这个参数有点意思。首先默认 0.75,这个若是不深究却是好解释,取了 0.5~1.0 的中间值嘛,可是若是我不按套路出牌设置为 2 ,HashMap会怎样呢?函数
上代码:测试
HashMap<String, String> testMap = new HashMap<>(2, 2);
testMap.put("test1", "val");
testMap.put("test2", "val");
testMap.put("test3", "val");
复制代码
初始 capacity 给了 2,loadFactor 也给了 2,那么 threshold 即为 4,容量是 2 ,到 4 才扩容,可是我 put 了三个元素进去容量已经超了,而且尚未触发扩容,会发生什么? 将断点打入 HashMap 的 putVal 方法,观察执行效果:大数据
if ((tab = table)i == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
复制代码
首先桶是空的,进行初始化的 resize,根据构造函数的参数,初始化的容量只有 2,紧着这进入第二个分支,进行与运算计算出数组的索引,一看 “嗯这地方没人,我 new 一个放这”。优化
也就是说,若是咱们构造函数传入的 loadFactor 大于 1,HashMap 并不会报错而是像 hash 冲突同样,追加到计算出的索引后方,提早造成了链表,查找元素的时间复杂度也就高了起来。ui
紧接着来看看第二个疑问,到底多大数据量,会出现红黑树? 首先 HashMap 有个重要的参数,就是TREEIFY_THRESHOLD 默认是 8,变量名很规范,一眼就能看出是干什么的,意如其名就是转为树的阈值,源码中的首次判断是否须要转为树,就是直接用的它:this
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 判断是否转为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
复制代码
上面这段代码也是 putVal 中的一部分,一般负责为 hash 冲突的节点追加链表元素,-1 for 1st这句是源码里自带的,由于 binCount 是从 0 开始嘛,因此阈值减一,解释了一下。 就像代码表述的,到了阈值将会调用treeifyBin方法,那么 treeifyBin 就会直接进行红黑树转换吗? NO! 咱们看看方法实现的前三行:
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
复制代码
方法先校验了 hash 桶的数量,要是小于MIN_TREEIFY_CAPACITY的话,从新调整大小便可,默认值是 64,也就是说并不仅是有TREEIFY_THRESHOLD一个参数的限制。 知道了从哪里转为红黑树,想要进行咱们的测试,最简单暴力的方式就是将 treeifyBin 方法第二个分支打个断点😋。 咱们将以前测试的构造函数删掉,采用默认的构造函数,写下测试代码:
HashMap<Integer, String> testMap = new HashMap<>();
int max = 1_000_00000;
for (int i = 0; i < max; i++) {
testMap.put(RandomUtil.randomInt(Integer.MAX_VALUE), "val");
}
复制代码
简单暴力,随机一千万个 key,当第一次进入咱们打的断点时,也就表明第一颗红黑树造成。点开 Idea 的 Evaluate 窗口,输入 this.size 就能看到当前 hashmap 的长度,我记录了十次第一个红黑树造成时的 size大小:
1. 2696228
2. 5685561
3. 559996
4. 2806127
5. 没出现
6. 6156770
7. 768019
8. 5577574
9. 739868
10. 8870967
复制代码
十次下来大概平均三百万数量级左右,会出现第一颗红黑树,固然这不是一次很是严谨的测试,统计次数,测试数据的随机分布性,都有可能影响结果,可是这个结果也能体现两点:
两点测试就到这里,咱们能够思考一下,既然数量级这么大才会出现红黑树,HashMap 还犯得上优化吗?笔者推测了一下,我的观点以下:
HashMap 咱们能够看到类 doc 的@since 注解是1.2,早在 jdk1.2 的时候 HashMap 就出现了,jdk1.2 是什么时间发布的呢,搜索可知是 1998 年!约 20 年前的电脑内存等配置和如今已是今非昔比,如今的内存条件,已经能够操做比当年更大更长的对象了,固然也别忘了 JVM 也要进行调优。
======================= 原创不易~点个赞再走吧!