HashMap小结
首先定义的我们的初始表的大小为16,这里是将1左移四位变成10000,即16;然后是我们的最大容量,负载因子0.75以及链表转换为树的长度8. 这里6是表示我们在进行扩容后选择要不要将红黑树转化为链表的阈值。64则是我们的初始表大小容量达到64是才会选择在链表过长的地方将他们改造成红黑树。
我们在hash表中主要存放的是NODE节点,它有以下属性,分别是hash值,key值,value值以及next指针。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
注意的是这里在计算hash值得时候采用了高位异或低位的方式进行计算,而这样做的主要原因是我们后面在进行寻址操作时采用hash&(table.length - 1)的方式进行计算的。
- 首先我们的table.length在前面提到了是2的幂次方,所以当我们将它进行-1操作的时候我们可以得到01111或011111等情况,但是这个时候我们的高16位是没有参与到我们的与运算当中的,所以我们如果单纯采用这样的寻址方式会有很多的hash冲突,所以我们将他的高位和低位进行了一个异或运算,减少hash冲突。
- 散列表一开始并不是按照我们的初始长度进行创建的,它是懒加载机制,在我们第一次进行put的时候才会进行初始扩容。这里是直接扩容2倍。除此之外我们需要判断当前的hash表长度是否达到了阈值(即负载因子*当前表容量),如果达到了也会进行扩容
- 在进行put的时候,我们需要进行一个判断,主要有四种情况。
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;
}
如果一开始表当前位置为空,我们直接就讲Node放到表的当前位置下。
- 如果表有值,我们就要进行判断,如果有值但是其中存在key相同,我们就可以直接更新节点,否则我们要继续进行判断.
- 如果当前槽位已经构成了一个红黑树,那么我们需要将值加入树中;
- 如果当前槽位构成的是链表那么我们就看加入节点之后链表长度是否达到了8,没有则将节点加入链表;
- 如果达到了我们就要进行建树的判断,如下图所示,当此时的hash表大小小于一开始设定的最小表长度时我们需要进行扩容,如果达到了最小表长度及64时,我们则进行建树操作。
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
然后我们在进行了扩容之后我们还需要对数据进行迁移,在迁移的时候我们也需要分情况进行一个讨论。
- 首先如果为空我们不管,如果不为空且后续没有树或者链表,那么我们可以利用寻址法hash&(table.length - 1)寻找新的地址,如果为树我们则需要调用树的split方法对数进行拆分,这里就用到了之前的UNTREEIFY_THRESHOLD,如果小于该值则将其转化为普通链表,否则重建红黑树。
- 如果此时为链表则我们还需要进一步进行判断,因为1.8在扩容后还是能够维持链表的原顺序。具体的实现流程我们可以从下图看出
我们一开始计算hash值是通过我们的hashcode与上table.length - 1来得到地址的,所以我们在同一链表的值在扩容后hash值要么不变要么等于原index加上原始表长度
java_hashmap美团技术