1.HashMap简介
一.什么是Hash?什么是HashMap?
Hash音译为“哈希”,直译为“散列”,是一种信息摘要算法,但他不是加密。散列函数(或散列算法,又称哈希函数,英语:Hash Function)是一种从任何一种数据中创建小的数字“指纹”的方法。散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来。该函数将数据打乱混合,重新创建一个叫做散列值(hash values,hash codes,hash sums,或hashes)的指纹(维基百科)。我们平时常用的MD5,SSL等都属于Hash算法,通过Key进行Hash的计算,就可以获取Key对应的HashCode。
众所周知数组这种数据结构便于查找,不便于插入删除;链表这种数据结构便于插入删除,不便于查找。那么有没有一种既便于查找又便于插入删除的结构呢,hashMap便出现了。在 JDK1.8 中,HashMap 是由 数组+链表+红黑树构成,在原来数组加链表的基础上新增了红黑树作为底层数据结构,结构变得复杂了,但是效率也变的更高效。
下面从源码开始分析hashMap
首先是属性介绍
2.1.默认的初始化容量。
2.2.map的最大容量
2.3.默认的加载因子,在扩容时使用。
2.4.桶中数量超过8时,会从链表变为红黑树
2.5.红黑树的节点少于6时会退化成链表
2.6.当Map里面的数量超过64时,表中的桶才能进行树形化 ,否则桶内元素太多时会进行扩容操作,而不是树形化 。为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
2.7hashMap内部采用Node存储每个节点的值。node实现了Entry接口
下面看下hashmap的构造方法
3.1HashMap()
构造一个空的 HashMap,默认初始容量(16)和默认负载因子(0.75)。
3.2HashMap(int initialCapacity)
构造一个空的 HashMap具有指定的初始容量和默认负载因子(0.75)。
3.3 HashMap(int initialCapacity, float loadFactor)
构造一个空的 HashMap具有指定的初始容量和负载因子。
仔细分析hashMap的初始化过程。首先会判断initialCapacity的大小是否小于0,小于零直接抛出IllegalArgumentException,再判断nitialCapacity是否大于最大值,大于最大值会将值设置为最大值,然后判断扩容因子是否小于0或者是否是非数值,是则抛出IllegalArgumentException,最后赋值。最后调用了一个tableSizeFor函数。
3.4 tableSizeFor(int cap),它会返回最小的大于给定目标容量的2的次幂。
下面分析put方法,调用putValue方法。
注意到key执行了一下hash方法,来看一下Hash方法是如何实现的。
这里将key的hashcode 和 h的高16位进行异或运算。在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。
3.5下面我们看看putVal方法,看看它到底做了什么。
它有五个参数:hash key的hash值,key 原始Key,value 要存放的值,onlyIfAbsent 如果true代表不更改现有的值,evict 如果为false表示table为创建状态
它的具体逻辑为,首先判断table是否为null或者长度是否为0,是的话进行扩容。
然后判断table[i]的值是不是为空,为空的话就创建一个NODE并赋值。
这边获取table的位置时候运用了(n-1) & hash。
这个n我们说过是table的长度,那么n-1就是table数组元素应有的下表。这个方法非常巧妙,它通过hash & (table.length -1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当length总是2的n次方时,hash&(length-1) 运算等价于对length取模,也就是hash % length,但是&比%具有更高的效率。
如果table[i]的值不为空,说明存在hash冲突,
首先与数组节点判断key值是否相等,如果相等,则直接取代。
在判断是否是树的节点,是的则插入为树的节点(跟新或插入)。关于红黑树的部分暂时不做分析。
否则为链表,分为两种情况:
1.遍历table[i],判断Key是否已存在:采用equals() 对比当前遍历节点的key 与 需插入数据的key:若已存在,则直接用新value 覆盖 旧value
2. 遍历完毕后仍无发现上述情况,则直接在链表尾部插入数据。
如果插入的时候覆盖了值,则返回旧值。
插入成功的情况下,最后判断是否需要扩容。
3.6 resize()
扩容方法有两种用法,一是用来初始化,二十用来扩容。
首先进行一些参数的设置,不仔细分析,重点分析后面参数移动的过程。
初始化新table后,遍历老table,将桶上的值依次加入新table中,如果桶中只有一个元素,则直接添加。
如果桶的元素是树的节点,调用树的spit方法添加。
如果是链表的节点,根据新参与运算的hashcode的值为0或1,将链表分别插入到新table的原位置和原位置+老的容量的位置上。
参考: