HashMap put值时第一次put和第二次put源码解析
HashMap源码还得经常看,了解过源码之后过一段时间还是会忘记,正好有时间再来回顾一下向HashMap put值时所走的流程。如有错误,欢迎各位大佬们指正。
第一次put值
当new一个HashMap的时候 hashMap的数组长度为0,第一次put值的时候才会给hashMap一个初始化的数组长度。
一、当第一次向HashMap put值时需要走以下代码
1、put( )
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
1.1、hash( )
static final int hash(Object key) {
int h;
//判断key是否为空,如果为空则返回0否则返回key.hashCode()异或上key.hashCode()向右位移16位的值
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
2、putVal( )
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判断tab是否为空如果为空则给数组分配一个长度走resize()方法
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//判断hash桶是否已被其他值占用,如果占用则走else,否则直接将key value放入node节点
if ((p = tab[i = (n - 1) & hash]) == null)
//new一个新的node节点,将key和value放进去,因为是第一次put所以next指针为null。
tab[i] = newNode(hash, key, value, null);
else{
/***************第一次put值时不走的代码已省略掉***************/
}
//经过结构修改的结构修改的次数+1,modCount是为hashmap修改的次数计数
++modCount;
//如果size+1大于threshold(数组长度*负载因子的值可称之为阈值)则需要给数组扩容
if (++size > threshold)
resize();
//这个方法是空的,在LinkedHashMap中,使用LRU算法,该方法有内容,实际是为了扩展其他
afterNodeInsertion(evict);
return null;
}
2.1 resize( )
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//如果老数组是否为null,是则长度为0否则为oldTab.length
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//将数组长度与负载因子的积(阈值)赋给oldThr
int oldThr = threshold;
int newCap, newThr = 0;
//第一次向hashMap put值oldCap肯定为0,所以此判断不走
if (oldCap > 0) {
...
}
//oldThr也不大于0,走else
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//将DEFAULT_INITIAL_CAPACITY(1向左位移4=16)的值赋给newCap
newCap = DEFAULT_INITIAL_CAPACITY;
//将DEFAULT_LOAD_FACTOR(负载因子)*DEFAULT_INITIAL_CAPACITY(16)的积赋给newThr
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//已经给newThr赋值,不走此代码
if (newThr == 0) {
...
}
//将newThr的值赋给阈值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//初始化newTab,给newTab一个长度
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//将空数组newTab赋给table
table = newTab;
//oldTab为空不走此代码
if (oldTab != null) {
...
}
return newTab;
}
2.2 newNode( )
// 创建一个node节点
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
至此,第一次put数据流程走完,HashMap的Node节点长度为16.
二、第二次put值
1、put
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
1.1、hash()
static final int hash(Object key) {
int h;
//判断key是否为空,如果为空则返回0否则返回key.hashCode()异或上key.hashCode()向右位移16位的值
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
2、putVal
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//由于第一次put时已将数据放入table中,此时table!=null所以不走
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//判断数组长度与上新key的hashcode是否已被占用,如果占用则hash冲突,走else,否则直接存值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
/***************此时else还不走,当出现hash碰撞时,才会走else代码***************/
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
2.1 newNode( )
// 创建一个node节点
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
至此第二次put数据流程走完,HashMap中经过两次put已将数据存放至hashmap中,虽然看似两次put所走的代码一样(除了第一次put时resize()了),但是只是没有出现hash冲突的情况,如果两次put的key存在hash冲突,则会将第二次的数据和第一次的数据放入同一个链表中。
三、hash冲突时
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//当出现hash冲突时tab[i = (n - 1) & hash]!=null,走else
else {
Node<K,V> e; K k;
//老key的hash值==新key的hash并且老key==新key的话会将新value覆盖老value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果key不相同,那么判断数据结构是否为红黑树,如果为红黑树则直接将新节点插入树中
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果还是链表的话,走for循环,循环的对象是p的next节点不为null或者p的后续节点有key相同,如果key相同,那么找到这个节点,用新值替换。否则,往链表的最后一点节点添加数据,因为遍历找链表,使用binCount来记录遍历个数,当遍历的个数达到门限值(默认是8)达到8个那么就要发生树化改造
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//判断链表长度是否大于树化负载因子如果大于则进行树化改造
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//正如上边所述key相同,则将value替换老的value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
四、总结
用大白话来说:
- 第一次put值时,先判断节点是否为空,如果为空则resize一个长度,并将key,value放入节点中。
- 第二次put值时,判断是否存在hash冲突,如果未冲突则直接将key,value放入节点中,如果出现hash冲突,则看key是否相同,相同则直接将新value替换旧value,不同则判断数据结构是否为树形结构,是则将节点插入树中,不是则为链表,便遍历链表知道next节点为null时将节点放入链表中,如果此时发现再插入此节点链表长度就大于树化负载因子的话,那么就对链表进行树化。