HashMap源码分析
1.HashMap简介
HashMap是JDK中Map的最常用的实现类。HashMap使用hash算法进行数据的存储和查询。
内部使用一个Entry表示键值对key-value。
用Entry的数组保存所有键值对, Entry通过链表的方式链接后续的节点(1.8后会根据链表长度决定是否转换成一棵树类似TreeMap来节省查询时间) Entry通过计算key的hash值来决定映射到具体的哪个数组(也叫Bucket)中。
我们这边博客主要讲解hashmap的2个重要方法(put方法+resize()扩容 )。
2.方法分析
2.1 PUT
/**
* 将指定值与此map中指定的键关联。如果键已经存在于map中,则替换键所关联的旧值。
*
* @param key 与指定值相关联的键。
* @param value 与指定键关联的值。
* @return 与键关联的上一个值,或null,如果没有键的映射。(A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent 如果为true,不改变已经存在的value
* @param evict 如果为false, 该表处于创建模式。
* @return 先前的值, 或者为null如果没有
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; //存放链表头结点的数组,数组的每个元素相当于一个桶,好像这个链表就存放于一个元素的空间中
Node<K,V> p; //存放于数组i处的节点
int n;//数组的长度
int i; //数组的下标值
//刚开始table是null或空的时候,调用resize()方法,初始化一个默认的table;为tab和n赋值,tab指向数组,n为数组的长度
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//用(n - 1) & hash计算出插入点在数组的下标。如果插入点为null,将此节点存放于此
//否则就会发生碰撞,此时将桶的头结点赋给p
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);//键值对的next为空
else {
Node<K,V> e; //相当于一个temp,一个暂时存放键值对的节点
K k; //同上
//hash值相等,key对象也相等,那么就是更新这个键值对的value。为什么要两者都满足,因为根据不同key对象的hashCode计算出来的hash可能相等,所以还需要通过比较引用("==")或者比较对象("equals")的方式判断。
//你可能要说那可以直接比较key对象就行,因为key相同,hash肯定相同。我们根据hash不同(p.hash == hash为false),可以判断出不是同一个key,我们知道符号"&&"有短路功能,所以整体为false,不用每次都去比较key,提高了效率。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;//我们将数组的头结点p赋给e,此时还没有将e插入数组的桶中
//我们可以从下一个else中知道,一个桶只能存放8个节点,大于八个将转成红黑树存储。根据桶中的Entry数,判断p的类型是否是TreeNode类的实例
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//如果是,使用putTreeVal方法插入Entry
//碰撞后,满足与桶中的第一个entry不等,且此时桶中Entry小于等于8个
else {
for (int binCount = 0; ; ++binCount) {//没有条件,通过break跳出循环
if ((e = p.next) == null) {//当p后没有节点时满足条件,此时桶中就一个Entry;或者此时p为桶中最后一个Entry
p.next = newNode(hash, key, value, null);//新建Entry链接上,此时e为null
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st//当桶中存放第8个元素时,将链表转换成红黑树。treeifyBin方法会在另一篇博文详解。
treeifyBin(tab, hash);
break;
}
//上面的if判断是否跟桶中的第一个Entry相等,而这个if是依次跟桶中的Entry比较
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;//如果此时桶中有多个Entry,执行完两个if后还没跳出循环,e=p.next,相当于p=p.next.继续循环.这个else最重要的一点是要理解---利用e,依次比较桶中的Entry.
}
}
if (e != null) { // existing mapping for key//e不等于null的条件是桶中存在相同的Entry提前跳出循环
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)//onlyIfAbsent默认为false,oldValue可以为null
e.value = value;//替换value
afterNodeAccess(e);//LinkedHashMap继承HashMap,此方法在LinkedHashMap中被重写.在这里没什么用,但是在LinkedHashMap中此处调用此方法移动节点到最后.
return oldValue;//存在相同Entry时返回oldValue.我原来使用put方法时没想到还有返回值,所以还是要看看源码
}
}
//HashMap继承Serializable,当HashMap被序列化时,transient变量不会被序列化
++modCount;//modCout是transient变量
//size也是transient变量,指Map中包含的键值对的数量。threshold默认为16*0.75
if (++size > threshold)//如果数组中的Entry大于等于length*0.75
resize();//调用resize方法将数组扩大为两倍
afterNodeInsertion(evict);//LinkedHashMap继承HashMap,此方法在LinkedHashMap中被重写.
return null;//默认返回空
上面的注释已经很清晰明了了,我们再来看看resize方法。
2.2扩容机制resize
resize负责初始化或者扩容内部数组,通过将数组大小扩大一倍。
resize大体过程为
首先计算resize()后的新的capacity和threshold值。如果原有的capacity大于零则将capacity增加一倍,否则设置成默认的capacity。
创建新的数组,大小是新的capacity
将旧数组的元素放置到新数组中。
final Node<K,V>[] resize() {
// 将字段引用copy到局部变量表,这样在之后的使用时可以减少getField指令的调用。
Node<K,V>[] oldTab = table;
// oldCap为原数组的大小或当空时为0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 新的数组的大小是旧数组的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 当旧的的数组大小大于等于默认大小时,threshold也扩大一倍。
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 按照新的capacity创建新数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 将原数组中的数组复制到新数组中
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
// 如果e是该bucket唯一的一个元素,则直接赋值到新数组中。
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// TreeNode的情况则使用TreeNode中的split方法将这个树分成两个小树
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 否则则创建两个链表用来存放要放的数据,hash值&oldCap为0的(即oldCap的1的位置的和hash值的同样的位置都是1,同样是基于capacity是2的次方这一前提)为low链表,反之为high链表, 通过这种方式将旧的数据分到两个链表中再放到各自对应余数的位置。
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 按照e.hash值区分放在loTail后还是hiTail后
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 处理完之后放到新数组中
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
结语
如果你哪里没理解到,欢迎留言。如果博文有误,欢迎指正。