Java集合之HashMap

关于HashMap的特点

1、HashMap是非线程安全的。

也就是在多线程下,使用HashMap不能保证存储的数据的正确性,也就是会出现多个线程同时操作一个数据,从而导致数据与设想的不一致。

2、HashMap初始化容量为16,每次扩容都是原来容量的两倍,扩容因子为0.75,底层是Hash表(JDK1.8之前是数组+链表,JDK1.8开始变为数组+链表+红黑树)。

1)也就是当元素的数量达到容器的百分之75时就会扩容。而为什么是0.75而不是其他的 ?

首先,0.75这个数值是研究人员利用统计学得到的最优解,当低于0.75,就会导致频繁扩容,扩容会降低效率;其次,也会导致空间过多浪费,降低空间利用率;当高于0.75,就会增加Hash冲突的发生概率,那么链表的长度会增加,接着红黑树也会变得更复杂,从而导致查询效率降低,也就是时间利用率降低。

2)为什么初始化容量为16?

 

  HashMap作为一种数据结构,元素在put的过程中需要进行hash运算,目的是计算出该元素存放在hashMap中的具体位置。

 

  hash运算的过程其实就是对目标元素的Key进行hashcode,再对Map的容量进行取模,而JDK 的工程师为了提升取模的效率,使用位运算代替了取模运算,这就要求Map的容量一定得是2的幂。

   

 X % 2^n = X & (2^n – 1)

 

  而作为默认容量,太大和太小都不合适,太小了就有可能频繁发生扩容,影响效率。太大了又浪费空间,不划算,所以16就作为一个比较合适的经验值被采用了。

   

  为了保证任何情况下Map的容量都是2的幂,HashMap在两个地方都做了限制。

 

  首先是,如果用户制定了初始容量,那么HashMap会计算出比该数大的第一个2的幂作为初始容量。

 

  另外,在扩容的时候,也是进行成倍的扩容,即4变成8,8变成16。

 

3)为什么建议设置hashmap的初始容量,设置多少合适?

 

为什么设置初始容量?

HashMap有扩容机制,就是当达到扩容条件时会进行扩容。HashMap的扩容条件就是当HashMap中的元素个数(size)达到或超过临界值(threshold)时就会自动扩容。在HashMap中,threshold = loadFactor * capacity,扩容是非常影响性能的。并且《阿里巴巴开发手册》中也是这么建议的:

 

设置多少合适?

有些人会自然想到,我准备塞多少个元素我就设置成多少呗。比如我准备塞7个元素,那就new HashMap(7),那么jdk就会为我们创建一个容量为8的 HashMap

但是,这个值看似合理,实际上并非如此。因为HashMap在根据用户传入的capacity计算得到的默认容量,并没有考虑到loadFactor这个因素,只是简单机械的计算出第一个大约这个数字的2的幂。

这么讲很多人可能不理解,请看下面:

如果我们需要用HashMap存7个元素,按照上面的策略,初始大小就会设置为7,经过JDK处理之后,HashMap的容量会被设置成8,但是,这个HashMap在元素个数达到 8*0.75 = 6的时候就会进行一次扩容,这明显是我们不希望见到的,那么到底设置为多少合适?

这里我们可以参考JDK8中putAll方法中的实现:

 

 

 

经过计算 7/0.75 + 1 = 10,10经过JDK处理之后,会被设置成16,这样就能大大的减少扩容的几率。

所以,我们可以认为,当我们明确知道HashMap中元素的个数的时候,把默认容量设置成 expectedSize / 0.75F + 1.0F 是一个在性能上相对好的选择。

当然,这种方式缺点也很明显,就是会牺牲内存。但是,现在硬件技术发达,我们认为内存属于比较丰富的资源,所以使用空间换时间是比较好的方案。

 

注:关于以上三点的回答,作者讲的比较粗略,详细的原理和过程感兴趣的读者可以自行查找资料)

 

3、HashMap的链表转红黑树的过程

相信很多小伙伴都记住了一句话,HashMap中,当链表长度达到8时就会将链表转为红黑树,我之前也是这么认为的,知道最近去翻看了HashMap的源代码,才发现这句话不正确。

是当链表长度不小于8时,且数组长度不小于64时,此时才会将链表长度不小于8的链表转换为红黑树,大家来看看源码吧。

 

 

 但是它有什么用呢?

 

 

当我们调用put方法时,底层其实调用了putVal方法,让我们来看看putVal方法

 

 

 接着我们来看看treeifyBin方法

 

 

 4、HashMap是无序且不重复的。

如果大家看了第三点就会知道为什么是不重复的,这里讲的再详细一点。

无序的:HashMap取值的时候是从数组位为0的链表开始顺序往下查询,接着再查找数组位为1......,存放数据时是直接映射到数组下标的,所以存数据的时候是无序的,而取数据时与存数据时的顺序不一致,那么取出来的数据当然是无序了。

不重复的:之所不重复,是因为调用了key的hashcode方法和equals方法(关于hashcode和hash值我就不详讲了,有兴趣的小伙伴可以去看看源码),不同的key可能有同样的hash值,而相同的key一定会有相同的hash值,如果hash值相同,那么就会定位到同一个数组位,这时候就会调用equals方法,如果重写了equals方法,就会比较对象的内容是否相同,如果相同就会覆盖,没有重写equals方法就会继承Object类的equals方法,去比较地址,如果地址相同也会覆盖,既然都覆盖了,那么也就不会出现重复了。

 

posted @ 2021-07-16 14:08  zheng_newbie  阅读(114)  评论(0编辑  收藏  举报