在Java基础中,集合类是很关键的一块知识点,也是平常开发的时候常常会用到的。好比List、Map这些在代码中也是很常见的。html
我的认为,关于HashMap的实现,JDK的工程师实际上是作了不少优化的,要说全部的JDK源码中,哪一个类埋的彩蛋最多,那我想HashMap至少能够排前五。java
也正是由于如此,不少细节都容易被忽视,今天咱们就来关注其中一个问题,那就是:面试
为何HashMap的负载因子设置成0.75,而不是1也不是0.5?这背后到底有什么考虑?算法
你们千万不要小看这个问题,由于负载因子是HashMap中很重要的一个概念,也是高端面试的一个常考点。api
另外,这个值得设置,有些人会用错的,好比前几天个人《阿里巴巴Java开发手册建议建立HashMap时设置初始化容量,可是多少合适呢?》这篇文章中,就有读者这样回复:数组
既然有人会尝试着去修改负载因子,那么到底改为1是否是合适呢?为何HashMap不使用1做为负载因子的默认值呢?数据结构
首先咱们来介绍下什么是负载因子(loadFactor),若是读者对这部分已经有了解,那么能够直接跨过这一段。oracle
咱们知道,第一次建立HashMap的时候,就会指定其容量(若是未显示制定,默认是16,详见为啥HashMap的默认容量是16?),那随着咱们不断的向HashMap中put元素的时候,就有可能会超过其容量,那么就须要有一个扩容机制。函数
所谓扩容,就是扩大HashMap的容量:优化
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length); }
createEntry(hash, key, value, bucketIndex);
}
复制代码
从代码中咱们能够看到,在向HashMap中添加元素过程当中,若是 元素个数(size)超过临界值(threshold)
的时候,就会进行自动扩容(resize),而且,在扩容以后,还须要对HashMap中原有元素进行rehash,即将原来通中的元素从新分配到新的桶中。
在HashMap中,临界值(threshold) = 负载因子(loadFactor) * 容量(capacity)。
loadFactor是装载因子,表示HashMap满的程度,默认值为0.75f,也就是说默认状况下,当HashMap中元素个数达到了容量的3/4的时候就会进行自动扩容。(相见HashMap中傻傻分不清楚的那些概念)
还记得前面咱们说过,HashMap在扩容到过程当中不只要对其容量进行扩充,还须要进行rehash!因此,这个过程实际上是很耗时的,而且Map中元素越多越耗时。
rehash的过程至关于对其中全部的元素从新作一遍hash,从新计算要分配到那个桶中。
那么,有没有人想过一个问题,既然这么麻烦,为啥要扩容?HashMap不是一个数组链表吗?不扩容的话,也是能够无限存储的呀。为啥要扩容?
这其实和哈希碰撞有关。
咱们知道,HashMap实际上是底层基于哈希函数实现的,可是哈希函数都有以下一个基本特性:根据同一哈希函数计算出的散列值若是不一样,那么输入值确定也不一样。可是,根据同一散列函数计算出的散列值若是相同,输入值不必定相同。
两个不一样的输入值,根据同一散列函数计算出的散列值相同的现象叫作碰撞。
衡量一个哈希函数的好坏的重要指标就是发生碰撞的几率以及发生碰撞的解决方案。
而为了解决哈希碰撞,有不少办法,其中比较常见的就是链地址法,这也是HashMap采用的方法。详见全网把Map中的hash()分析的最透彻的文章,别无二家。
HashMap将数组和链表组合在一块儿,发挥了二者的优点,咱们能够将其理解为链表的数组。
HashMap基于链表的数组的数据结构实现的
咱们在向HashMap中put元素的时候,就须要先定外到是数组中的哪条链表,而后把这个元素挂在这个链表的后面。
当咱们从HashMap中get元素的时候,也是须要定位到是数组中的哪条链表,而后再逐一遍历链表中的元素,直到查找到须要的元素为止。
可见,HashMap经过链表的数组这种结构,解决了hash冲突的问题。
可是,若是一个HashMap中冲突过高,那么数组的链表就会退化为链表。这时候查询速度会大大下降。
因此,为了保证HashMap的读取的速度,咱们须要想办法尽可能保证HashMap的冲突不要过高。
那么如何能有效的避免哈希碰撞呢?
咱们先反向思惟一下,你认为何状况会致使HashMap的哈希碰撞比较多?
无外乎两种状况:
一、容量过小。容量小,碰撞的几率就高了。狼多肉少,就会发生争强。
二、hash算法不够好。算法不合理,就可能都分到同一个或几个桶中。分配不均,也会发生争强。
因此,解决HashMap中的哈希碰撞也是从这两方面入手。
这两点在HashMap中都有很好的提现。两种方法相结合,在合适的时候扩大数组容量,再经过一个合适的hash算法计算元素分配到哪一个数组中,就能够大大的减小冲突的几率。就能避免查询效率低下的问题。
至此,咱们知道了loadFactor是HashMap中的一个重要概念,他表示这个HashMap最大的满的程度。
为了不哈希碰撞,HashMap须要在合适的时候进行扩容。那就是当其中的元素个数达到临界值的时候,而这个临界值前面说过和loadFactor有关,换句话说,设置一个合理的loadFactor,能够有效的避免哈希冲突。
那么,到底loadFactor设置成多少算合适呢?
这个值如今在JDK的源码中是0.75:
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
复制代码
那么,为何选择0.75呢?背后有什么考虑?为何不是1,不是0.8?不是0.5,而是0.75呢?
在JDK的官方文档中,有这样一段描述描述:
As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put).
大概意思是:通常来讲,默认的负载因子(0.75)在时间和空间成本之间提供了很好的权衡。更高的值减小了空间开销,但增长了查找成本(反映在HashMap类的大多数操做中,包括get和put)。
试想一下,若是咱们把负载因子设置成1,容量使用默认初始值16,那么表示一个HashMap须要在"满了"以后才会进行扩容。
那么在HashMap中,最好的状况是这16个元素经过hash算法以后分别落到了16个不一样的桶中,不然就必然发生哈希碰撞。并且随着元素越多,哈希碰撞的几率越大,查找速度也会越低。
另外,咱们能够经过一种数学思惟来计算下这个值是多少合适。
咱们假设一个bucket空和非空的几率为0.5,咱们用s表示容量,n表示已添加元素个数。
用s表示添加的键的大小和n个键的数目。根据二项式定理,桶为空的几率为:
P(0) = C(n, 0) * (1/s)^0 * (1 - 1/s)^(n - 0)
复制代码
所以,若是桶中元素个数小于如下数值,则桶多是空的:
log(2)/log(s/(s - 1))
复制代码
当s趋于无穷大时,若是增长的键的数量使P(0) = 0.5,那么n/s很快趋近于log(2):
log(2) ~ 0.693...
复制代码
因此,合理值大概在0.7左右。
固然,这个数学计算方法,并非在Java的官方文档中提现的,咱们也无从考察到底有没有这层考虑,就像咱们根本不知道鲁迅写文章时候怎么想的同样,只能推测。这个推测来源于Stack Overflor(stackoverflow.com/questions/1…
理论上咱们认为负载因子不能太大,否则会致使大量的哈希冲突,也不能过小,那样会浪费空间。
经过一个数学推理,测算出这个数值在0.7左右是比较合理的。
那么,为何最终选定了0.75呢?
还记得前面咱们提到过一个公式吗,就是临界值(threshold) = 负载因子(loadFactor) * 容量(capacity)
。
咱们在《为啥HashMap的默认容量是16?》中介绍过,根据HashMap的扩容机制,他会保证capacity的值永远都是2的幂。
那么,为了保证负载因子(loadFactor) * 容量(capacity)
的结果是一个整数,这个值是0.75(3/4)比较合理,由于这个数和任何2的幂乘积结果都是整数。
HashMap是一种K-V结构,为了提高其查询及插入的速度,底层采用了链表的数组这种数据结构实现的。
可是由于在计算元素所在的位置的时候,须要使用hash算法,而HashMap采用的hash算法就是链地址法。这种方法有两个极端。
若是HashMap中哈希冲突几率高,那么HashMap就会退化成链表(不是真的退化,而是操做上像是直接操做链表),而咱们知道,链表最大的缺点就是查询速度比较慢,他须要从表头开始逐一遍历。
因此,为了不HashMap发生大量的哈希冲突,因此须要在适当的时候对其进行扩容。
而扩容的条件是元素个数达到临界值时。HashMap中临界值的计算方法:
临界值(threshold) = 负载因子(loadFactor) * 容量(capacity)
复制代码
其中负载因子表示一个数组能够达到的最大的满的程度。这个值不宜太大,也不宜过小。
loadFactor太大,好比等于1,那么就会有很高的哈希冲突的几率,会大大下降查询速度。
loadFactor过小,好比等于0.5,那么频繁扩容没,就会大大浪费空间。
因此,这个值须要介于0.5和1之间。根据数学公式推算。这个值在log(2)的时候比较合理。
另外,为了提高扩容效率,HashMap的容量(capacity)有一个固定的要求,那就是必定是2的幂。
因此,若是loadFactor是3/4的话,那么和capacity的乘积结果就能够是一个整数。
因此,通常状况下,咱们不建议修改loadFactor的值,除非特殊缘由。
好比我明确的知道个人Map只存5个kv,而且永远不会改变,那么能够考虑指定loadFactor。
可是其实我也不建议这样用。咱们彻底能够经过指定capacity达到这样的目的。详见为啥HashMap的默认容量是16?
参考资料:
stackoverflow.com/questions/1…