HashMap源码揭秘
1,put方法逐行解释:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {//table数组是否为空,如果为空(即new的hashMap()没有指定hashMap的大小),
inflateTable(threshold);//则进行初始化
}
if (key == null)//HashMap对象的key可以为空
return putForNullKey(value);//如果key为空了,采用另一种策略
int hash = hash(key);//对key的hashCode值进行一次hash()函数,来算出新的hash值,对key.hashCode进行hash函数是为了减少碰撞
int i = indexFor(hash, table.length);//根据hash值,来算出key对应的数组的下标,常见的方式有取模,但这里不是
for (Entry<K,V> e = table[i]; e != null; e = e.next) {//如果下标所在的数组位存在,遍历第i个位置上的链表
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//看链表上是否有要找的key,如果有,则返回老的值
V oldValue = e.value; //如果链表上已将有了要设置的key,则先取到老的value,进行保存,让新的value赋给key
e.value = value; //用新的value来覆盖掉老的value
e.recordAccess(this);//这是个空方法
return oldValue; //返回老的value
}
}
modCount++;
addEntry(hash, key, value, i);//前插法,插入元素
return null;
}
2.put方法中调用的重要的方法解析:
(1)第25行的addEntry(hash, key, value, i);//前插法,插入元素
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);
}//if语句和扩容有关
createEntry(hash, key, value, bucketIndex);//这一步才是插入元素的关键
}
其中 createEntry(hash, key, value, bucketIndex); 采用头插法,来插入新的k-v键值对方法如下
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];//老的头结点
table[bucketIndex] = new Entry<>(hash, key, value, e);//关键
size++;
}
其中 table[bucketIndex] = new Entry<>(hash, key, value, e);分为两步执行
(1), new Entry<>(hash, key, value, e);
new一个新的Entry 对象,并把它next指向老的头结点所在的那一串链表,
即让新创建的Entry的next赋值为e。
(2) 让新的头结点去指向新的链表,即让new Entry<>(hash, key, value, e);赋值给 table[bucketIndex]
前插法的过程
why 要选择头插法?
因为尾插法,你还要遍历链表去找到最后一个尾结点,与头插法相比,比较浪费时间
采用头插法的话,只需要让新插入即new出的Entry对象的节点的next指向老的头结点,
让新的头结点指向新的结点
3.indexFor(int n,int length)方法,来取一个 key应该放在数组的那个下标位置,首先index应该小于length-1,然后再考虑如何均匀地分配
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);
}
(1).h是经过hash()函数运算后的hash值),而length是一个2的整数次幂(这和初始化数组时有关)的数,减去1之后,会发现它的最低位全为1,
indexFor方法相当于取模操作,1.要保证数组下标不越界,2,要保证尽量的平均,减少冲突,一个数字%(length-1),
(2).例如一个int[10]数组,给定一个数2,让他%(10-1),等于2,就把它放在数组的第二个位置,
(3).也就是说,尽可能的让一个数存放在下标等于它自己的值的位置,又因为任何数和全为1的数相&都为他们本身,
(4).所以 h &(length-1)能够实现数组不会越界,尽可能地实现平均,减少碰撞
4.hash( Object key)函数,主要是算出k的h值,
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
该函数的作用是让高位参与,让k生成的h在进行求数组下标运算的过程中尽可能的使k(让高位参与&运算)均匀地放在数组中,减少hash冲突