Java架构师交流群:793825326
java版本:jdk1.8
IDE:idea 18
上一篇(https://blog.csdn.net/dap769815768/article/details/96584394)我们分析了HashMap的get方法,下面我针对目前的源码分析对HashMap做下总结:
1.往链表里面追加数据是从尾部加入,即
p.next = newNode(hash, key, value, null);
红黑树亦是如此,因此红黑树操作仍然用到了next这个属性。
2.hashmap由于每次存入数据都要进行扩容和链表转红黑树检查,所以存数据的速度很慢,通过控制加载因子可以控制扩容的频率,那么一般场景下什么样的加载因子合适呢,参考如下:
a)加载因子越大,越会频繁地触发链表转红黑树操作,而扩容的操作次数则越小,空间利用率加大,这个时候hashmap更像是一个链表或者红黑树,涉及到的链表和红黑树查询变多,需要比较key是否相等的次数变多,查询速度降低,存储速度快。
b)加载因子越小,触发链表转红黑树的概率越低,扩容操作次数则越大,空间利用率降低,这个时候的hashmap更像是一个数组,涉及到的链表和红黑树查询较少,查询速度变快,存储速度慢。
c)官方的默认值是0.75,这个是空间利用率和效率的一个折衷,至于为何0.75是最佳值,暂时我还不清楚。
综上,加载因子所决定地是hashmap的数组特性多一些还是链表/红黑树的特性多一点,哪个特性多一点,那么它使用起来就越接近哪种数据类型。
对此,我做了一个测试,如下:
HashMap<String,String> map=new HashMap(8);
long start=System.currentTimeMillis();
for (int i=0;i<10000000;i++)
{
map.put(Integer.toString(i),Integer.toString(i));
}
System.out.println(System.currentTimeMillis()-start);
由于运行环境的差异,同样的代码可能每次打印的时间都不太一样,当使用默认加载因子的时候,上面的代码普遍打印的时间是11000-12000之间,当加载因子设置为1时,普遍打印时间为9000-10000之间。但并不是值越大越快,当值继续增大后,触发红黑树操作的概率就加大了,红黑树操作比链表要耗时,因此存入的时间会变长。
总结hashmap的存数据操作耗时的环节如下:
a)扩容操作
b)链表转红黑树操作
c)红黑树操作
d)key值的比较,加载因子越小,hash碰撞的概率越小,比较的次数越少,越省时(对应查询速度越快),但触发扩容操作的概率越高
由于HashMap的查询时间很快,不管采用多大的加载因子,一般都是ms级一下,所以这里面不比较加载因子对于查询速度的影响了
所以理论上来讲,要想提高HashMap的数据存入速度,在提前知道大概数据有多少的情况下,可以考虑实例化的时候设置容量来减少resize的次数,提高速度。但这个不是绝对的,参考如下测试代码:
HashMap<String,String> map=new HashMap();
long start=System.currentTimeMillis();
for (int i=0;i<20000000;i++)
{
map.put(Integer.toString(i),Integer.toString(i));
}
System.out.println("存入时间:"+(System.currentTimeMillis()-start));
这段代码的普遍耗时大概在24000-25000之间,如果设定初始容量为20000000,代码耗时反倒变长了,因为数组变大,导致数组的寻址开销变多。如果使用5000000,则普遍执行时间在22000左右,效率会提高一些。所以初始容量的选择,要以尽量减少扩容为原则,尤其是后期的扩容,当数据量很大的时候,每次扩容都将耗费大量的时间,但是容量太大,数组的寻址则会变得很耗时,所以初始容量的设定还要兼顾尽量减少寻址耗时。
另外这个还和你的数据有关,如果你的数据恰好规律一致,经过hash计算后,全部发生了碰撞,那么就会有大量的红黑树操作,这个时候的查询速度显然不会太快,录入速度也不会有明显的提升。因为要进行大量的key的比较。
因此总结下来,初始容量和加载因子配合使用,才能有效控制扩容次数,控制寻址时间,控制红黑树的操作频率等,而这些算是比较耗时的操作。
所以我的建议是,使用HashMap的时候,加载因子尽量使用默认的0.75,这大概是官方通过实验得出的一个比较理想的数据,初始容量的设置尽量接近实际数据。
3.链表转红黑树的触发条件是,链表的节点数大于等于8并且hash桶的大小大于等于最小树形化阈值64的时候,才进行红黑树转化,否则就进行扩容操作。所以理论上来讲,可能存在链表长度大于等于8 的情况,因为可能扩容后,原本的整条链还在同一索引位置上,并没有被拆成两条链。如果在等于8的情况下往该条链追加数据,由于尚未达到树形化条件,那么则继续扩容,但继续扩容仍然未能将原本的两条链拆开,这个时候链表长度便为9。(这里涉及到hash算法的问题,需要验证)
4.扩容的时候,可能存在某个数的节点个数很小的情况,则进行红黑树转链表操作,这个阈值是6。
5.put方法会返回旧值。
6.hashmap查询快,插入慢。适合的场景是大数据情况下的查询操作,而不是大数据情况下的插入操作。
7.get方法为何参数类型是object类型,而不是泛型E?这个问题暂时找不到答案,后面再补充。