HashMap原始碼分析(四)put-jdk8-紅黑樹的引入
HashMap
jdk8以後他的邏輯結構發生了一點變化:
大概就是這個意思:
當某一個點上的元素數量打到一定的閾值的時候,連結串列會變成一顆樹,這樣在極端情況下(所有的元素都在一個點上,整個就以連結串列),一些操作的時間複雜度有O(n)變成了O(logn)。
分析原始碼;
一.還是先看下put方法,證明一下上面的圖基本是對的:
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //如果當前map中無資料,執行resize方法。並且返回n if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //如果要插入的鍵值對要存放的這個位置剛好沒有元素,那麼把他封裝成Node物件,放在這個位置上就完事了 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //否則的話,說明這上面有元素 else { Node<K,V> e; K k; //如果這個元素的key與要插入的一樣,那麼就替換一下,也完事。 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //1.如果當前節點是TreeNode型別的資料,執行putTreeVal方法 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //還是遍歷這條鏈子上的資料,跟jdk6沒什麼區別 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //2.完成了操作後多做了一件事情,判斷,並且可能執行treeifyBin方法 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) //true || -- e.value = value; //3. afterNodeAccess(e); return oldValue; } } ++modCount; //判斷閾值,決定是否擴容 if (++size > threshold) resize(); //4. afterNodeInsertion(evict); return null; } // Callbacks to allow LinkedHashMap post-actions void afterNodeAccess(Node<K,V> p) { } void afterNodeInsertion(boolean evict) { } void afterNodeRemoval(Node<K,V> p) { }
3.4點:
這2個方法是空的,註釋上寫著這是為LinkedHashMap(HashMap的子類)留的回撥函式。方便LinkedHashMao的可開發,所以不管他。
2.if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
這個方法就是判斷一下當前走到的這個地方(連結串列位置),長度是否達到閾值,需要把連結串列換成樹的形式
也就是執行treeifyBIn方法,當然這裡要是換成樹的形式的話,裡面的元素肯定也要換成TreeNode型別的
1.if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
所以說這裡就很好理解了,看下你是不是樹結構的,如果是數結構就以樹的形式去處理,而不是以之前的單鏈表的形式,傻傻的去一個個遍歷
可以看到。put方法與jdk6相比,多出了有關treeNode的處理,同時也少了key為null的判斷,記得1.6中的key==null是首先判斷並且給他找位置的。
這個多虧了hash方法,他的傳入引數從原先的int值的hashCode變成了物件,支援null的傳入,所以null傳進去得到0的hashCode也能當做普通值一般化處理了
還有之前的indefFor()方法消失 了,直接用(tab.length-1)&hash,所以看到這個,代表的就是陣列的下角標。static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
二.TreeNode
簡單的說,不管方法,裡面就包含4個屬性。
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
三.treeifyBin
這個方法就是將容器中的node變成treeNode。
方法的引數是Node[] ,int hash,指定需要tree化的是哪個陣列,對應的哪個下角標所連出來的連結串列。
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();
//Node e=tab[該hash對應的角標],e就是這個角標下的第一個元素。
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
//replacementTreeNode == new TreeNode(),就是包裝了一個TreeNode物件
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
//遍歷連結串列上的第一個元素的時候,t1==null,將p賦值給hd
//也就是先記錄一下,方便後面的元素記錄pre,next
hd = p;
else {
//現在p是個tree了,pre記錄上一個元素
p.prev = tl;
//順便把自己的引用在上一個元素上做記錄
tl.next = p;
}
//將當前操作的元素的引用傳遞給t1
tl = p;
//遍歷整個連結串列,直到沒有元素。
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
//遍歷完了,再執行hd.treeify方法
//hd=p是在t1==null時執行,也就是隻有在第一個元素的時候執行了一次
//所以hd代表的是這個樹的根。
hd.treeify(tab);
}
}