首先聊其基本组成单位Node
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
基本组成hash,key,value,next指针。注意这里没有setKey方法。
再从put方法讲起,put方法内部调用putVal这个final方法
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);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof 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) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
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;
}
首先讲主体逻辑(空间足够,没有扩容,无覆盖),首先查到key位置,插入,如果数量大于等于树化阈值,则树化,否则为链表。
逐步添加特殊情况,
1、table为null或者table长度为0,则需要先建好表,这需要调用resize方法
2、查到key对应index位置,value为null,即此槽无Node,那么直接newNode,覆盖
3、查到位置不为空,那么需要考虑到key相同的情况,分为两类,内存地址相同或者equals,
如果相同直接覆盖,
4、需要对槽内节点进行依次比较,在此过程中,分为两类,槽内节点组织方式为树或者链表。
5、若节点组织方式为树,调用putTreeVal方法
6、若节点组织方式为链表,沿着链表不停寻找,如果p(pre)的next为空,代表所有节点已验证过不行,执行插入操作,插入之后如果binCount大于等于阈值减一,则需要开始树化了。如果不为空,那么比较比较,如果两个key一致,结束,否则的话p(pre)移动到下一个节点。
7、在这种情况下,e代表是待插入的节点,如果为null,代表之前没有,如果不为null,代表是替换,这个时候执行替换因为默认为false,无需考虑其他。
8、在插入之后,如果size大于阈值,resize。
下面开始一个一个地解析,
resize
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
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)
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"})
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)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
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;
}
梳理整体框架,容量加倍,数据搬移。
容量加倍
1、oldTab为null的情况即为0,不为null的情况或者length。
2、这个时候如果oldTab容量已经大于最大容量(1<<30),阈值为Integer.MAX_VALUE。直接返回
否则的话,如果加倍后小于最大容量而且原来
Todo,涉及到容量和阈值两个变化,而且有些时候还是不同步的,得画张图,理解具体的场景!
数据搬移
讲主体过程,oldTab不为null的情况。
首先对oldTab进行遍历。如果Node为null,略过。如果Node不为null,如果其next为null,即只有Node中只有一个槽,直接转移,老的位置为null。而index的计算是e.hash&(newCap-1)。
如果Node中槽聚合成树,那么使用split方法
如果Node中槽为链表的法,因为老的tab不为null,则得加倍。那么正好链表中的槽分为两类,一类index不动,另一类索引+oldCap这么多,这也是容量必须加倍的原因所在。而区分的依据在这里
(e.hash & oldCap) == 0
它的原理也很容易理解,前后hash&区别在于多出一个1。以这个来区别lo和hi,而index恰好相差oldCap这么多。
那么依次搬移的过程很容易理解了,不过其中有小细节,它不是用dummyNode,而是if else的方式来做,考虑到hashmap这种非常常用的结构,节省了内存。
而最后链表尾为null,略过,不为null,尾巴next为null,然后newTab[j]=新的头。
getNode
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
getNode的逻辑比较直接,首先tab不null,而且tab.length得大于0而且tab索引位置不为null。
分为三种情况,只有Node中只有一个槽,树和链表。
如果直接找到Ok,找不到,而且next!=null,那么如果它是树,那么在树中找,如果不是树,在链表中一个个找,直到到null,或者返回找到的槽。
都不行,返回null。
removeNode
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
依然是那个流程,首先tab不为null而且长度必须大于0而且Node不为null,这里可以锁定索引了。
此时,先和当前Node比较,比较的流程依然是hash,(内存地址,equals),之所以这么安排是因为首先有hash相同再有后面的,而内存地址在前面比较,是因为,内存地址比较会更快,这个顺序的技巧挺有意思的。
如果不相同,那么首先它的next不为null,然后在里面找,那么又是分为两类,树和链表,在其中查找,链表,当前是返回,否则,继续直到为null,这里有一个p(pre),代表失败的最后一个节点。
之后如果node不为null,那么看matchValue是否为false,再看,value内存地址是否相同,再看不为null的情况下,是否equals,这是一个范式!!!
依然是范式,如果是树节点,那么调用树的方法remove,如果不是树的节点。
那么分为两种,一个是链表头删除,即p==node,那么tab[index]=node.next
否则p.next=node.next;
modCount++;--size;返回node。
否则返回null;
replace
@Override
public V replace(K key, V value) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
在getNode方法上做了封装,如果不为null,那么覆盖并返回原来的值。
resize部分之后再看!!