面试的时候闻到了Hashmap的扩容机制,以前只看到了Hasmap的实现机制,补一下基础知识,讲的很是好html
原文连接:java
http://www.iteye.com/topic/539465面试
Hashmap是一种很是经常使用的、应用普遍的数据类型,最近研究到相关的内容,就正好复习一下。网上关于hashmap的文章不少,但究竟是本身学习的总结,就发出来跟你们一块儿分享,一块儿讨论。
一、hashmap的数据结构
要知道hashmap是什么,首先要搞清楚它的数据结构,在java编程语言中,最基本的结构就是两种,一个是数组,另一个是模拟指针(引用),全部的数据结构均可以用这两个基本结构来构造的,hashmap也不例外。Hashmap其实是一个数组和链表的结合体(在数据结构中,通常称之为“链表散列“),请看下图(横排表示数组,纵排表示数组元素【其实是一个链表】)。
从图中咱们能够看到一个hashmap就是一个数组结构,当新建一个hashmap的时候,就会初始化一个数组。咱们来看看java代码:算法
首先算得key得hashcode值,而后跟数组的长度-1作一次“与”运算(&)。看上去很简单,其实比较有玄机。好比数组的长度是2的4次方,那么hashcode就会和2的4次方-1作“与”运算。不少人都有这个疑问,为何hashmap的数组初始化大小都是2的次方大小时,hashmap的效率最高,我以2的4次方举例,来解释一下为何数组大小为2的幂时hashmap访问的性能最高。
看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,可是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就须要遍历这个链表,获得8或者9,这样就下降了查询的效率。同时,咱们也能够发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费至关大,更糟的是这种状况中,数组可使用的位置比数组长度小了不少,这意味着进一步增长了碰撞的概率,减慢了查询的效率!
因此说,当数组长度为2的n次幂的时候,不一样的key算得得index相同的概率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的概率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
说到这里,咱们再回头看一下hashmap中默认的数组大小是多少,查看源代码能够得知是16,为何是16,而不是15,也不是20呢,看到上面annegu的解释以后咱们就清楚了吧,显然是由于16是2的整数次幂的缘由,在小数据量的状况下16比15和20更能减小key之间的碰撞,而加快查询的效率。
因此,在存储大容量数据的时候,最好预先指定hashmap的size为2的整数次幂次方。就算不指定的话,也会以大于且最接近指定值大小的2次幂来初始化的,代码以下(HashMap的构造方法中):编程
总结:
本文主要描述了HashMap的结构,和hashmap中hash函数的实现,以及该实现的特性,同时描述了hashmap中resize带来性能消耗的根本缘由,以及将普通的域模型对象做为key的基本要求。尤为是hash函数的实现,能够说是整个HashMap的精髓所在,只有真正理解了这个hash函数,才能够说对HashMap有了必定的理解。数组
三、hashmap的resize
当hashmap中的元素愈来愈多的时候,碰撞的概率也就愈来愈高(由于数组的长度是固定的),因此为了提升查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操做也会出如今ArrayList中,因此这是一个通用的操做,不少人对它的性能表示过怀疑,不过想一想咱们的“均摊”原理,就释然了,而在hashmap数组扩容以后,最消耗性能的点就出现了:原数组中的数据必须从新计算其在新数组中的位置,并放进去,这就是resize。
那么hashmap何时进行扩容呢?当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认状况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,而后从新计算每一个元素在数组中的位置,而这是一个很是消耗性能的操做,因此若是咱们已经预知hashmap中元素的个数,那么预设元素的个数可以有效的提升hashmap的性能。好比说,咱们有1000个元素new HashMap(1000), 可是理论上来说new HashMap(1024)更合适,不过上面annegu已经说过,即便是1000,hashmap也自动会将其设置为1024。 可是new HashMap(1024)还不是更合适的,由于0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 咱们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。
四、key的hashcode与equals方法改写
在第一部分hashmap的数据结构中,annegu就写了get方法的过程:首先计算key的hashcode,找到数组中对应位置的某一元素,而后经过key的equals方法在对应位置的链表中找到须要的元素。因此,hashcode与equals方法对于找到对应元素是两个关键方法。
Hashmap的key能够是任何类型的对象,例如User这种对象,为了保证两个具备相同属性的user的hashcode相同,咱们就须要改写hashcode方法,比方把hashcode值的计算与User对象的id关联起来,那么只要user对象拥有相同id,那么他们的hashcode也能保持一致了,这样就能够找到在hashmap数组中的位置了。若是这个位置上有多个元素,还须要用key的equals方法在对应位置的链表中找到须要的元素,因此只改写了hashcode方法是不够的,equals方法也是须要改写滴~固然啦,按正常思惟逻辑,equals方法通常都会根据实际的业务内容来定义,例如根据user对象的id来判断两个user是否相等。
在改写equals方法的时候,须要知足如下三点:
(1) 自反性:就是说a.equals(a)必须为true。
(2) 对称性:就是说a.equals(b)=true的话,b.equals(a)也必须为true。
(3) 传递性:就是说a.equals(b)=true,而且b.equals(c)=true的话,a.equals(c)也必须为true。
经过改写key对象的equals和hashcode方法,咱们能够将任意的业务对象做为map的key(前提是你确实有这样的须要)。数据结构
总结:
本文主要描述了HashMap的结构,和hashmap中hash函数的实现,以及该实现的特性,同时描述了hashmap中resize带来性能消耗的根本缘由,以及将普通的域模型对象做为key的基本要求。尤为是hash函数的实现,能够说是整个HashMap的精髓所在,只有真正理解了这个hash函数,才能够说对HashMap有了必定的理解。app
虽然在hashmap的原理里面有这段,可是这个单独拿出来说rehash或者resize()也是极好的。编程语言
何时扩容:当向容器添加元素的时候,会判断当前容器的元素个数,若是大于等于阈值---即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。函数
扩容(resize)就是从新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组没法装载更多的元素时,对象就须要扩大数组的长度,以便能装入更多的元素。固然Java里的数组是没法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像咱们用一个小桶装水,若是想装更多的水,就得换大水桶。
咱们分析下resize的源码,鉴于JDK1.8融入了红黑树,较复杂,为了便于理解咱们仍然使用JDK1.7的代码,好理解一些,本质上区别不大,具体区别后文再说。
newTable[i]的引用赋给了e.next,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置;这样先放在一个索引上的元素终会被放到Entry链的尾部(若是发生了hash冲突的话),这一点和Jdk1.8有区别,下文详解。在旧数组中同一条Entry链上的元素,经过从新计算索引位置后,有可能被放到了新数组的不一样位置上。
下面举个例子说明下扩容过程。
这句话是重点----hash(){return key % table.length;}方法,就是翻译下面的一行解释:
假设了咱们的hash算法就是简单的用key mod 一下表的大小(也就是数组的长度)。
其中的哈希桶数组table的size=2, 因此key = 三、七、5,put顺序依次为 五、七、3。在mod 2之后都冲突在table[1]这里了。这里假设负载因子 loadFactor=1,即当键值对的实际大小size 大于 table的实际大小时进行扩容。接下来的三个步骤是哈希桶数组 resize成4,而后全部的Node从新rehash的过程。
下面咱们讲解下JDK1.8作了哪些优化。通过观测能够发现,咱们使用的是2次幂的扩展(指长度扩为原来2倍),因此,
通过rehash以后,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。对应的就是下方的resize的注释。
看下图能够明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key肯定索引位置的示例,图(b)表示扩容后key1和key2两种key肯定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。
元素在从新计算hash以后,由于n变为2倍,那么n-1的mask范围在高位多1bit(红色),所以新的index就会发生这样的变化:
所以,咱们在扩充HashMap的时候,不须要像JDK1.7的实现那样从新计算hash,只须要看看原来的hash值新增的那个bit是1仍是0就行了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,能够看看下图为16扩充为32的resize示意图:
这个设计确实很是的巧妙,既省去了从新计算hash值的时间,并且同时,因为新增的1bit是0仍是1能够认为是随机的,所以resize的过程,均匀的把以前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。有一点注意区别,JDK1.7中rehash的时候,旧链表迁移新链表的时候,若是在新表的数组索引位置相同,则链表元素会倒置,可是从上图能够看出,JDK1.8不会倒置。有兴趣的同窗能够研究下JDK1.8的resize源码,写的很赞,以下: