1.什么是HashMap?
HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫Entry.这些个键值对分散存储在一个数组中,这个数组就是HashMap的主干,数组的每个元素初始化为null,
1,为什么用了一维数组:数组存储区间是连续的,占用内存严重,故空间复杂度很大。但数组的查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难
2,为什么用了链表:链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易而HashMap是两者的结合,用一维数组存放散列地址,以便更快速的遍历;用链表存放地址值,以便更快的插入和删除!
2.HashMap的Put Get方法
Put
调用put方法例如调用hashMap.put("apple",0),会在数组中插入一个key为"apple"的元素,通过一个hash函数确定Entry的插入位置index=hash("apple")
但是数组的长度有限,可能会发生index冲突,HashMap数组的每一个元素是一个Entry,也是一个链表的头结点,当新来的Entry映射发生冲突时,会使用头插法(发明者认为后插入的Entry被查找的可能性大),将其插入链表中,每一个Entry对象通过Next指针指向它的下一个结点
Get
首先会将输入的key做一次映射,得到对应的index,查看头结点的key是否是target,如果是就找到了;如果不是就查看next结点
3.HashMap长度
HashMap默认初始长度16,并且每次自动扩展或者手动初始化时,长度必须是2的幂
Hash: index=HashCode(Key)&(length-1);
如果长度不是2的整数幂,会增加重复几率的
而且2的整数幂-1其实二进制都是1,相当于取HashCode的后几位,尽量保证HashCode分布均匀
4.HashMap的扩充
HashMap的容量是有限的,经过多次的插入,会达到一定的饱和度,Key映射位置发生冲突的几率提高,所以要Resize扩充HashMap数组
Resize的因素:
Capacity:HashMap的当前长度
LoadFactor:HashMap得负载因子,默认值为0.75f
HashMap是否能进行Resize的条件:
HashMap.size >= Capacity * LoadFactor
Resize步骤:
扩容:创建一个新的Entry空数组,长度是原来的两倍
ReHash:遍历原来的数组,把所有的Entry重新Hash到新数组.
ReHash源码:
/** * Transfers all entries from current table to newTable. */ void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } } }
5.HashMap出现环的原理
正常的Rehash过程
假设表size=2,key=3,7,5 这里所使用的rehash算法就是index=key&(size-1)
所以key=3,5,7的Entry对象都指向了数组index=1的位置
而capacity=3,size=2,size<capacity*0.75
所以数组应进行扩容.
上面有数组扩容时,table rehash到newTable的代码,这里只摘取while循环的代码
while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; }
Rehash过程图:
并发下的Rehash过程
假设有两个线程:线程A和线程B
1.线程A执行一行代码后挂起
while(null != e) {
Entry<K,V> next = e.next; //线程A执行完这句时挂起
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
2.线程B全部执行完
然而线程A: e指向key:3,next指向key:7,e.next=next;
线程B: e指向key:3,next指向key:7,next.next=e
3.这时线程A抢到了执行权,回来继续执行
e.next=newTable[i]; //导致key3.next指向了null newTable[i]=e; //线程A的index3指向了key3 e=next; //e指向了key7
4.因为线程A执行的结果:key3.next=key7,所以下一次循环将key7插在key3所在链的头,然后e和next都往下移
5.执行e.next=newTable[i];导致key3.next=key7,所以出现环形链表