一、什么是HashMap
HashMap肯定是一个Java开发者经常用到的一个容器,不妨吧HashMap看成是Hash + Map
Hash:散列讲一个任意的长度通过某种算法(哈希算法)转换为一个固定值
Map:这个Map被翻译过来是地图,地图是一种很明显的K-V模型,地图上的每一个点对应真实存在的某一个地方,所以我认为是一种K-V模型
好了两个概念合二为一即是:把一个东西通过哈希算法找到对应的位置,并把它存储到这个位置!
二、HashMap源码分析
下面的部分是转载自IT小猛男
在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。
而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值 8 时,将链表转换为红黑树,这样大大减少了查找时间。
HashMap的实现原理
首先有一个每个元素都是链表(可能表述不准确)的数组,当添加一个元素(key-value)时,就首先计算元素key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,但是形成了链表,同一各链表上的Hash值是相同的,所以说数组存放的是链表。而当链表长度太长时,链表就转换为红黑树,这样大大提高了查找的效率
HashMap的原理图是:
1、JDK1.8中的涉及到的数据结构
①、位桶数组
1 | transient Node<k,v>[] table;//存储(位桶)的数组</k,v> |
②、数组元素Node<K,V>实现了Entry接口
1 | //Node是单向链表,它实现了Map.Entry接口 |
③、红黑树
1 | //红黑树 |
2、源码中的数据域
加载因子(默认0.75):为什么需要使用加载因子,为什么需要扩容呢?因为如果填充比很大,说明利用的空间很多,如果一直不进行扩容的话,链表就会越来越长,这样查找的效率很低,因为链表的长度很大(当然最新版本使用了红黑树后会改进很多),扩容之后,将原来链表数组的每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置,这样就减少了每个链表的长度,增加查找效率
HashMap本来是以空间换时间,所以填充比没必要太大。但是填充比太小又会导致空间浪费。如果关注内存,填充比可以稍大,如果主要关注查找性能,填充比可以稍小。
1 | public class HashMap<k,v> extends AbstractMap<k,v> implements Map<k,v>, Cloneable, Serializable { |
3、HashMap的构造函数
HashMap的构造方法有4种,主要涉及到的参数有,指定初始容量,指定填充比和用来初始化的Map
1 | //构造函数1 |
4、HashMap的存取机制
①、HashMap如何getValue值,看源码
1 | public V get(Object key) { |
get(key)方法时获取key的hash值,计算hash&(n-1)得到在链表数组中的位置first=tab[hash&(n-1)],先判断first的key是否与参数key相等,不等就遍历后面的链表找到相同的key值返回对应的Value值即可
②、HashMap如何put(key,value);看源码
1 | public V put(K key, V value) { |
下面简单说下添加键值对put(key,value)的过程:
1、判断键值对数组tab[]是否为空或为null,否则以默认大小resize();
2、根据键值key计算hash值得到插入的数组索引i,如果tab[i]==null,直接新建节点添加,否则转入3
3、判断当前数组中处理hash冲突的方式为链表还是红黑树(check第一个节点类型即可),分别处理
5、HasMap的扩容机制resize()
构造hash表时,如果不指明初始大小,默认大小为16(即Node数组大小16),如果Node[]数组中的元素达到(填充比*Node.length)重新调整HashMap大小 变为原来2倍大小,扩容很耗时。
1 | final Node<K,V>[] resize() { |
6、JDK1.8使用红黑树的改进
在Java jdk8中对HashMap的源码进行了优化,在jdk7中,HashMap处理“碰撞”的时候,都是采用链表来存储,当碰撞的结点很多时,查询时间是O(n)。
在jdk8中,HashMap处理“碰撞”增加了红黑树这种数据结构,当碰撞结点较少时,采用链表存储,当较大时(>8个),采用红黑树(特点是查询时间是O(logn))存储(有一个阀值控制,大于阀值(8个),将链表存储转换成红黑树存储)
7、问题分析
你可能还知道哈希碰撞会对hashMap的性能带来灾难性的影响。如果多个hashCode()的值落到同一个桶内的时候,这些值是存储到一个链表中的。最坏的情况下,所有的key都映射到同一个桶中,这样hashmap就退化成了一个链表——查找时间从O(1)到O(n)。
随着HashMap的大小的增长,get()方法的开销也越来越大。由于所有的记录都在同一个桶里的超长链表内,平均查询一条记录就需要遍历一半的列表。
JDK1.8HashMap的红黑树是这样解决的:
如果某个桶中的记录过大的话(当前是TREEIFY_THRESHOLD = 8),HashMap会动态的使用一个专门的treemap实现来替换掉它。这样做的结果会更好,是O(logn),而不是糟糕的O(n)。
它是如何工作的?前面产生冲突的那些KEY对应的记录只是简单的追加到一个链表后面,这些记录只能通过遍历来进行查找。但是超过这个阈值后HashMap开始将列表升级成一个二叉树,使用哈希值作为树的分支变量,如果两个哈希值不等,但指向同一个桶的话,较大的那个会插入到右子树里。如果哈希值相等,HashMap希望key值最好是实现了Comparable接口的,这样它可以按照顺序来进行插入。这对HashMap的key来说并不是必须的,不过如果实现了当然最好。如果没有实现这个接口,在出现严重的哈希碰撞的时候,你就并别指望能获得性能提升了。
哈希函数
高16位与低16位做异或运算
1 | static final int hash(Object key) { |
- 本文作者: Tim
- 本文链接: https://zouchanglin.cn/2019/07/28/641628342.html
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 许可协议。转载请注明出处!