HashMap是底层结构是数组+链表,数组是Node(Entry)数组,链表是Node组成的链表,充分利用了Node的特性。链表的头结点存放在数组中。
put方法的大致思路
- 调用hash(key)方法,根据key对象的hashCode计算出kay的hash,根据hash计算出其在tab数组中的index,将键和值放入Node(Entry)中;
- 如果index位置的Node为空,称为没碰撞,直接Node放入数组中;
- 如果碰撞(index位置存在Node),如果key已经存在,替换old value(先跟头结点比较,key不相等时,循环链表比较),否则将Node链接到链表最后。
- 如果碰撞导致链表的长度大于等于7,将链表的结构转换为红黑树。
- 如果bucket存放的current capacity(当前容量)超过容量的load factor(0.75),就resize,容量扩大为两倍。
源码
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
//使用16位无符号右位移(>>>)异或(^)混合生成一个hash,替代key的hashCode,hash混合hashCode的高位和低位,以此来加大低位的随机性,用于减少碰撞。
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* @param onlyIfAbsent 如果为true,不改变已经存在的value
* @param evict 如果为false, 该表处于创建模式。
* @return 先前的值, 或者为null如果没有
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; //存放链表头结点的数组,数组的每个元素相当于一个桶,好像这个链表就存放于一个元素的空间中
Node p; //存放于数组i处的(头)节点
int n;//数组tab的长度
int i; //数组tab的下标值
//刚开始table是null或空的时候,调用resize()方法,初始化一个长度为16的tab
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//用(n - 1) & hash计算出插入点在数组的下标。如果插入点为null,将此节点存放于此。(&为长运算符,使用在计算boolean表达式时,会强制计算&两边的算式。此处用作位运算,即将n-1及hash均转为二进制数,相加,同为1则结果为1,否则为0,结果始终在[0,n-1])
//(第2个key跟第1个key相同时,必定hash值相同,index也相同)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);//键值对的next为空
//否则就会发生碰撞
else {
Node e; //相当于一个temp,一个暂时存放键值对的节点
K k; //一个temp,暂时存放key的值
//跟数组头结点p比较,如果key的hash值相等,key对象也相等。为什么要两者都满足,因为根据不同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,用于更改头结点的value
//我们可以从下一个else中知道,一个桶只能存放8个节点,大于八个将转成红黑树存储。根据桶中的Entry数,判断p的类型是否是TreeNode类的实例
else if (p instanceof TreeNode)
e = ((TreeNode)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(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方法将tab数组扩大为两倍
afterNodeInsertion(evict);//LinkedHashMap继承HashMap,此方法在LinkedHashMap中被重写.
return null;//默认返回空
}
测试代码
public static void main(String[] args) {
HashMap map = new HashMap<>();
for (int i = 0; i <= 20; i++) {
String key = "key" + i;
String value = "value" + i;
map.put(key, value);
}
for (int i = 10; i <= 20; i++) {
String key = "key" + i;
String value = "value" + i + 1;
map.put(key, value);
}
for (int i = 0; i <= 20; i++) {
String key = "key" + i + 1;
String value = "value" + i + 1;
map.put(key, value);
}
}
参考
- Java 1.8 HashMap 源码中 put()方法详解
- JDK 源码中 HashMap 的 hash 方法原理是什么?