HashMap.put(K key, V value)
,当调用HashMap的put方法插入时,你知道HashMap底层是如何将元素插入到数组列表中的吗?今天,我们就通过对HashMap底层代码的解读,探寻这个问题。
首先,HashMap的put方法通过调用putVal逻辑实现,下面是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:节点数组,p: 数组指定位置的节点
if ((tab = table) == null || (n = tab.length) == 0) // 判断当前HashMap有没有存储空间
n = (tab = resize()).length; // 扩容
if ((p = tab[i = (n - 1) & hash]) == null) // 通过hash计算的节点位置,判断数组当前位置有没有其他节点p
tab[i] = newNode(hash, key, value, null); // 没有则将节点放到数组该位置
else { // 当前数组该位置有节点
Node<K,V> e; K k; // e: 要添加的节点;k:键
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 如果指定位置处有节点p,且p的hash与要添加的节点的hash一样,并且p的键与要添加的键一样
e = p; // 要添加的节点e 指向 p
else if (p instanceof TreeNode) // 如果数组指定位置处节点p是TreeNode子类
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 调用红黑树的插入方法
else {
for (int binCount = 0; ; ++binCount) { // 遍历当前位置链表所有的节点
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null); // 位置最后插入节点
if (binCount >= TREEIFY_THRESHOLD - 1) // 链表长度大于等于7
treeifyBin(tab, hash);// 链表转换为红黑树 (需要桶容量大于64,否则执行扩容)
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 判断链表中有无相同节点
break;
p = e;
}
}
if (e != null) { // 数组中已有该节点
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) // onlyIfAbsent:如果为true,表示可以修改原key旧值为新值;这里表示如果可以修改旧值,或旧值为null,就修改旧值为新值
e.value = value;
afterNodeAccess(e);
return oldValue; // 返回旧值
}
}
++modCount;
if (++size > threshold) // 长度大于阈值
resize(); // 扩容
afterNodeInsertion(evict);
return null;
}
我们先将源码的逻辑拆分,整理出如下几步:
- 检查容量,扩容
- 将元素存储进数组列表
- 如果数组列表处没有元素,直接添加
- 如果数组列表处有元素
- 当前元素与要添加的元素的key相同,指针指向该元素
- 是TreeNode节点,则调用putTreeVal添加元素,如果红黑树中有key相同元素,指针指向该元素
- 如果都不是,则遍历链表
- 链表中有元素与要添加的元素key相同,指针指向该元素
- 没有,则将元素添加到最后
- 如果指针指向不为null,表示相同位置下,有key重复元素。
- 如果onlyInAbsent=false或重复元素的值为null,则用新值替换旧值
- 检查容量,扩容
源码难点解析:
- i = (n - 1) & hash
n - 1
是数组列表的长度-1,因为HashMap的capacity是一定为2的整数次方的,所以-1后,二进制就是以01111111…的形式存在的,这样与hash进行&的时候就会取到不超过数组长度的数。比如:capacity = 16 => 10000,n-1 = 01111,hash=10111101110100101011, (n-1)&hash=1011=11<16.
补充:
为什么HashMap的capacity一定是2的整数次方,因为HashMap在初始化的时候,如果传入initCapacity,就会执行tableSizeFor(int cap)方法
static final int tableSizeFor(int cap) { // cap = 17 --> 10001
int n = cap - 1; // n = 10000
n |= n >>> 1; // n = 10000 | 01000 = 11000
n |= n >>> 2; // n = 11000 | 00110 = 11110
n |= n >>> 4; // n = 11110 | 00001 = 11111
n |= n >>> 8; // n = 11111 | 00000 = 11111
n |= n >>> 16; // n = 11111 | 00000 = 11111
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
通过一系列逻辑或方法,将n转成011111…的格式,最后加一就是1000000…,是2的整数次方。