如上,基本的特性在代码里面注释了,HashMap实现了Map接口,是一个基于散列表的Map类,Map接口的特性就是存储键值对。散列表是一种存储结构,它可以通过散列函数直接访问到目标数据值,所以在定位下标方面可认为为o(1)。
1 HashMap继承自AbstractMap类同时实现了Cloneable,Serializable这两个接口,其中前一个接口Cloneable是为了实现clonet()机制,Serializable接口是为了实现序列化机制,关于这两种机制的相关知识再此不做赘述。
2 HashMap用到了泛型来实现参数化类型,其实java中的全部集合框架都使用到了泛型。
上述代码中解释了HashMap中重要字段的意思,相信大家一看就会有大概理解了。
由于在Java8的实现中,当经过hash函数计算得出的下标地址冲突到一定范围时,就会 把冲突的数据用链表的形式连起来,而当用链表数据大于一定范围时,就会将链表转化为红黑树存储。
首先HashMap会有一个基准数组table:
其中transient关键字是用来让该域在整个类被序列化的时候不包含该内容,即该域不被序列化
重点说一下static final float DEFAULT_LOAD_FACTOR = 0.75f;这个属性,该属性即为装载因子,本质上就是我们学习数据结构中解决Hash冲突的填充因子的意思,它的默认值是0.75,如果实际元素所占容量占分配容量的75%时就需要扩容。
第一步,table是一个数组,所以会有下标,HashMap首先会根据传入每个节点的(key,value)中的key,算出应该放到哪一个下标的数组中。
第二步,如果此下标数组为null,那么就直接放入,不为null,就走到第三步。
我们来看一下HashMap的构造器
可以看到java设计者们重载了4个HashMap的构造器,重点关注一下tableSizeFor(),putMapEntries这两个函数,我们来看一下tableSizeFor的源码:
table容量为2的倍数时,有利于下一步计算table的下标,
另一个方面,虽然在HashMap中,提供了一个构造方法:
public HashMap(int initialCapacity, float loadFactor)
看似提供了初始容量的方法,但是这个方法最后一行代码中调用了另一个方法tableSizeFor
来确定table的容量:
tableSizeFor
方法保证 函数返回值是>=给定参数initialCapacity
最小的2的幂次方的数值。
n |= n >>> 16
n
继续无符号右移16
位。n | (n >>> 16)
,导致n
二进制表示高17~32
位经过运算值均为1
;
目前n
的高1~32
位均为1
。
cap(cap < MAXIMUM_CAPACITY )
的值是多少,经过以上运算,其值的二进制所有位都会是1
。再将其加1
,这时候这个值一定是2
的幂次方。当然如果经过运算值大于MAXIMUM_CAPACITY
,直接选用MAXIMUM_CAPACITY
。所以最终table的length 即后来会提到的n 只能是2的倍数。
在HashMap中要注意区分hashCode和hash两个方法
hashCode返回的是一个32位的2进制数值。
HashMap
中存储数据table
的index
是由key
的Hash
值决定的。在HashMap
存储数据的时候,我们期望数据能够均匀分布,以避免哈希冲突。自然而然我们就会想到去用%
取余的操作来实现我们这一构想。
取余(%
)操作中 如果除数是2的幂次方则等同于与其除数减一的与(&
)操作。
tab[index] =tab[(n - 1) & e.hash] //即等同于index=e.hash % n;
其中hash=hash(key),n=table.length
。
n
是2
的幂次方,那么n- 1
的高位应该全部为0
。如果e.hash
值只用自身的hashcode
的话,那么index
只会和e.hash
低位做&
操作。这样一来,index
的值就只有低位参与运算,高位毫无存在感,从而会带来哈希冲突的风险。key
的哈希值的时候,用其自身hashcode
值与其低16
位做异或操作。这也就让高位参与到index
的计算中来了,即降低了哈希冲突的风险又不会带来太大的性能问题。log(n)-1
位,会得到一个范围在0~table.length的值,这个值,就是数组的下标。是不是很有艺术。由于hash是由key的hashCode的高16位与低16位经过异或而得,混合了原始哈希码的高低位,大大的提升了随机性,也让碰撞机率大大降低。
综上可以看出为什么Table容量只能是2的倍数~~~
n为2的整数幂保证了n-1最后一位(当然是二进制表示)为1,从而保证了取索引操作 h&(n-1)的最后一位同时有为0和为1的可能性,保证了散列的均匀性。反过来讲,当Hash表长度n为奇数时,n-1最后一位为0,这样与h按位与的最后一位肯定为0,即索引位置肯定是偶数,这样数组的奇数位置全部没有放置元素,浪费了大量空间。
总之:n为2的幂保证了按位与 最后一位的有效性,使哈希表散列更均匀。
接着我们来看一下:putMapEntries这个函数:
首先获得传入的map实例的大小s,然后存在一个将大小s与临界值比较的过程
如果map实例的 > threshold ,则调用resize()方法,即扩容,我们来看一下resize()的源码:
即扩容机制包括两部分:1 HashMap中table数组的容量的扩容
2 成员属性threshold(即扩容的临界值)的更改。
Note:
加载因子(默认0.75):为什么需要使用加载因子,为什么需要扩容呢?
因为如果填充比很大,说明利用的空间很多,如果一直不进行扩容的话,链表就会越来越长,这样查找的效率很低,因为链表的长度很大(当然最新版本使用了红黑树后会改进很多),扩容之后,将原来链表数组的每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置,这样就减少了每个链表的长度,增加查找效率
HashMap本来是以空间换时间,所以填充比没必要太大。但是填充比太小又会导致空间浪费。如果关注内存,填充比可以稍大,如果主要关注查找性能,填充比可以稍小。
构造hash表时,如果不指明初始大小,默认大小为16(即Node数组大小16),如果Node[]数组中的元素达到(填充比*Node.length)重新调整HashMap大小 变为原来2倍大小,扩容很耗时。
所涉及到的数据结构链表Node与红黑树TreeNode了,这两个数据结构为HashMap中的内部类,源码如下:
可以看到该链表Node是一个单向链表(因为只存在一个Node<K,V> next属性)它实现了Map.Entry<K,V>接口。
即HashMap是采用数组 Node<K,V>[ ] table来存储<K,V>的,数组中的每个元素是Node类型(可能会将该Node类型转换为TreeNode类型),通常称这种方式为位桶+链表/红黑树,当某个位桶的链表的长度达到TREEIFY_THRESHOLD临界值的时候,这个链表就将转换成红黑树。本质上是一个Hash表,用来解决冲突的(这一点将在HashMap中的put<K,V>方法中看到)。用图示表示如下:
在jdk8中,HashMap处理“碰撞”增加了红黑树这种数据结构,当碰撞结点较少时,采用链表存储,当较大时(>8个),采用红黑树(特点是查询时间是O(logn))存储(有一个阀值控制,大于阀值(8个),将链表存储转换成红黑树存储)
HashMap
实现了Map
接口,Map
接口设置一系列操作Map
集合的方法,如:put
、get
、remove
...等方法,而HashMap
也针对此有其自身对应的实现。
HashMap
继承AbstractMap
类。AbstractMap
类对于Map
接口做了基础的实现,实现了containsKey
、containsValue
...等方法。
1 put(K,V):
在构造函数中最多也只是设置了initialCapacity
、loadFactor
的值,并没有初始化table
,table
的初始化工作是在put
方法中进行的。
put方法主要包括两大部分:
1 根据传入的key计算hash值得到插入的数组索引 i,如果tab[i]==null,表示此下标处无元素存在,可直接添加元素,否则出现冲突,那么就要用到链表或红黑树来解决冲突,可参看上图帮助理解,
2 如果出现冲突,则扫描链表或红黑树,在此过程中通过equals方法来确定是否存在该元素,如果存在,则直接更新,否则采用链表或红黑树的方式将元素添加到tab[i]对应的链表或红黑树中,可参看上图帮助理解。
即通过hash的值来判断是否存在该元素,如果hash值不存在(tab[i]==null),则一定不存在该元素,若hash值存在,则可能存在该元素,需要通过equals方法来确定,如果hash值存在且key.equals.(k)则表明存在该元素,直接更新其值,否则表明不存在,则采用链表或红黑树的方式将元素添加到tab[i]对应的链表或红黑树中
Note: 如果key=null,那么hash(key)=0,table[0] 位置存在 ,且初始时 存放的是null值(有判断是否存放null值的步骤)。
2 V get(Object key)
从上述代码可以看到:
get某个元素与put某个元素是一一对应的关系:
先通过key得到对应的hash值,然后通过该hash值与table的长度n-1相与得到数组下标的索引first,
然后先判断传入的hash是否与数组索引first节点对应的hash值相等,
如果是则直接返回该数组元素first,
否则则通过first.next不断查找该数组元素所对应的链表/红黑树中是否存在hash与key均和传入的hash与key相等的节点,
如果存在则代表在HashMap集合中找到了该元素,则返回其对应的Value。
1 HashMap内部是基于Hash表实现的,该Hash表为Node类型数组+链表/红黑树,其中链表与红黑树是用来解决冲突的,即当往HashMap中put某个元素时,相同的hash值的两个值会被放到数组中的同一个位置上形成链表或红黑树。
2 HashMap存在扩容机制,是通过resize()方法实现的,即当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,数组的大小*loadFactor=threshold(即扩容的临界值),默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍
3 另外从put与get的源码可以看到HashMap的Key与Value都允许为null,同时可以看到HashMap中的put与get方法均无synchronized关键字修饰,即HashMap不是线程安全的。
4 HashMap中的元素是唯一的(即同一个key只存在唯一的V与之对应),因为在put的过程中如果可能出现相同元素(K相同V不同),则原来的V将会被替换。
你可能还知道哈希碰撞会对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来说并不是必须的,不过如果实现了当然最好。如果没有实现这个接口,在出现严重的哈希碰撞的时候,你就并别指望能获得性能提升了。
原文:https://www.cnblogs.com/mayundalao/p/12463045.html