作为初学者,简单对HashMap的原理做一下总结(以1.8之前的HashMap为主)
数组
数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;
链表
链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。
哈希表
那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表。哈希表((Hash table)既满足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便。
HashMap结合了数组和链表的速度优势
(index)0 | (table)entry | |||
1 | entry | entry | entry | ... |
2 | entry | |||
3 | entry | |||
... | ... |
- key,value是以entry的形式保存,entry中还保存了其他的数据,如next,hash等
- HashMap的内部首先是一个entry[]数组(即示意图中的table列)
- 多个entry可能存在同一角标中,这时这些entry就以链表的形式链接(即后加入的entry放在entry[]数组(table列)里,之前的entry放在后加入的entry的next中)
- HashMap中定义了一个hash()方法,防止小容量HashMap中的key产生过多碰撞(即存入同一角标中)
- HashMap初始大小是16,加载因子是0.75,当HashMap的容量达到初始大小*加载因子时会扩容,扩容到原来的二倍
- HashMap有构造函数可以指定初始容量和加载因子
- 如果可以,尽量减少HashMap的扩容次数,因扩容很耗时.
- JDK1.8中,如果满足条件(entry[]的长度大于64,单个链表长度大于8),会将链表转为红黑树,增加读写效率.
- JDK1.8中,使用了Node(extends Entry)这个新的内部类来替代Entry,内部只有少量代码优化(如使用了新的句法语句),实现基本相同.也许是为了与红黑树更匹配所以使用Node这个名字吧..
Entry类里面有一个next属性,作用是指向下一个Entry。打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。也就是说数组中存储的是最后插入的元素。
用例子来说明HashMap的内部结构:
- 一个key/value对是以entry的形式存入HashMap的entry[]数组中
- entry中保存了key,value,hash,next
- 这个entry在数组中的角标(index)由key运算得来(int index = key.hashCode()&(entry[].length-1))
- 如果多个key/value对都存在同一个index下,那么将entry(新)放在entry[]数组中,并让entry(新)的next等于entry(原)
其他:关于遍历entry的for循环很有趣,之前我以为for循环只能for(int i=0;i<10;i++)这种,但源码里使用了for(Entry<K,V> e = table[i]; e != null; e = e.next)的方式,真是学到了(实现了链表形式;用出了循环的新高度),只能说自己基础太差....
附:
HashMap 的底层原理(相比前两个浅显些)
图解数组和链表(补基础:数组和链表各自的原理/优缺点,解释了操作的时间复杂度概念(O(1)/O(n)等),很好的一篇文章)