HashMap---1.7源码阅读
JDK1.7
注意点:1、缺省值length为空,初始大小默认为16.每次扩容为原来的2倍,负载因子为0.75.扩容阈值为16*0.75=12,是超过12的时候进行扩容
2、数组+链表。冲突时,头插法。先扩容,再插入(1.8先插入再扩容)
3、key为null,且数组不为空,则放在table[0]的位置
4、hashcode&(length-1)为数组下标。扩容后,元素要么在原位置,要么在原位置+原length的位置
5、hashMap大小必是2的幂次方数。目的是尽量较少碰撞,也就是要尽量把数据分配均匀
1、数组 存放元素的是Entry<K,V>[] table数组,初始为空
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE
无参构造方法,默认空,插入元素的时候初始化为16,负载因子为0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
2、put方法
public V put(K key, V value) {
if (table == EMPTY_TABLE) { //数组为空,则先初始化
inflateTable(threshold);
}
if (key == null) //key为null,则方法
return putForNullKey(value);
int hash = hash(key); //计算key的hashcode值,先去key的hashcode,再经过4次位移运算和5次异或运算,得到hashcode
int i = indexFor(hash, table.length); //与运算,得到放在table的数组下表
for (Entry<K,V> e = table[i]; e != null; e = e.next) { //该数组位置无参数则直接放,有参则遍历链表,key的hashcode相等且equals相同则覆盖,
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) { //已有key为null的值则覆盖
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0); //无key为null,则放在0的位置
return null;
}
final int hash(Object k) { //计算key的hashcode值
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) { //利用hashcode和数组长度-1“与”运算得到数组位置
return h & (length-1);
}
void addEntry(int hash, K key, V value, int bucketIndex) {//插入方法
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
数组为空,则先初始化
key为null则判断,已有key为null则覆盖,无key为null则放在table数组的0位置
计算key的hashcode值,先去key的hashcode,再经过4次位移运算和5次异或运算,得到hashcode
hashcode和数组长度进行与运算h & (length-1),得到应放的数组下标
该数组位置无参数,则直接放
有参则遍历链表,key的hashcode相等且equals相同则覆盖,无相同则使用头插法,插到链表头部
插入前先判断,是否需要扩容,如果要,则先扩容,在放元素
注:先扩容,再插入。头插法。因为是头插法,多线程的情况下会导致死循环。头插法速度比尾插法快
3、get方法
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
private V getForNullKey() {
if (size == 0) {
return null;
}
for (Entry<K,V> e = table[0]; e != null; e = e.next) { //数组下表为0的元素
if (e.key == null)
return e.value;
}
return null;
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
先判断key是否为null,是则判断数组是否为空,空则返回null.非空则返回table[0]
key不是null,先判断数组是否为空,是则返回null.
不是则先计算key的hash值,在找到通过hash&(length-1)数组下标找到对应链表
遍历链表,hashcode相同并且keyequals相同时,找到元素,返回value
4、扩容
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
每次扩容为原来的2倍
遍历数组和链表,重新计算元素的hashcode和位置,依次放入元素。
补充:扩容后的元素,要么在原位,要么在原位+原数组长度=新位置。1.7是每次都重新计算,1.8做了优化,直接计算高位是否为1,为1则原位+原数组长度=新位置,为0则在原位置
头插法,元素倒叙
5、hashMap的中table数组位置的计算方法
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
问:为什么要进行^运算和>>运算呢?因为要让hashcode的高位数参数计算,减少hash碰撞
举例:length=16 length-1=15 : 0000 1111
h: :1111 0011
length-1:0000 1111
0000 0011 结果为3,放在数组【3】的位置
再为进行>>和^运算时,hashcode只有地位进行了运算,高位是什么不影响结果,因此要通过5次异或运算和4次位移运算使hashcode尽量分散,减少碰撞
6、hashMap的大小,始终为2的幂次方,无论带参设置多少,都取大于该值得最小2的幂次方
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
问:为什么要保证为2的幂次方?
因为hashcode&(length-1)=数组位置,只有当length为2的幂次方时,length-1才能是低位为都为1的二进制,进行与运算才不会数组越界。因为都为1111才有与运算的意义,如果都为0,则无论如何都为0,会加剧hashcode的碰撞
16:0001 0000
15:0000 1111
问:为什么扩容后的数组元素,要么在原位置,要么为原位置+原来length的位置?
length*2-1 实质上是二进制的高位从0变成1
15: 0000 1111
31:0001 1111
进行与运算时,要么是1,要么是0。是1,则原位置+原来length的位置。是0则位置不变
7、多线程下,扩容变成死循环的问题 参考:https://www.cnblogs.com/devilwind/p/8044291.html
根本原因:头插法,会使原有元素倒叙
线程1:扩容后,遍历元素前停止
线程2:扩容后,遍历元素,顺利完成
浙公网安备 33010602011771号