【集合系列】- 深刻淺出分析HashMap

1、摘要

在集合系列的第一章,我們瞭解到,Map的實現類有HashMap、LinkedHashMap、TreeMap、IdentityHashMap、WeakHashMap、Hashtable、Properties等等。html

關於HashMap,一直都是一個很是熱門的話題,只要你出去面試,我保證必定少不了它!node

本文主要結合JDK1.7和JDK1.8的區別,就HashMap的數據結構和實現功能,進行深刻探討,廢話也很少說了,直奔主題!面試

2、簡介

在程序編程的時候,HashMap是一個使用很是頻繁的容器類,它容許鍵值都放入null元素。除該類方法未實現同步外,其他跟Hashtable大體相同,但跟TreeMap不一樣,該容器不保證元素順序,根據須要該容器可能會對元素從新哈希,元素的順序也會被從新打散,所以不一樣時間迭代同一個HashMap的順序可能會不一樣。算法

HashMap容器,實質仍是一個哈希數組結構,可是在元素插入的時候,存在發生hash衝突的可能性;編程

對於發生Hash衝突的狀況,衝突有兩種實現方式,一種開放地址方式(當發生hash衝突時,就繼續以此繼續尋找,直到找到沒有衝突的hash值),另外一種是拉鍊方式(將衝突的元素放入鏈表)Java HashMap採用的就是第二種方式,拉鍊法。數組

在jdk1.7中,HashMap主要是由數組+鏈表組成,當發生hash衝突的時候,就將衝突的元素放入鏈表中。數據結構

從jdk1.8開始,HashMap主要是由數組+鏈表+紅黑樹實現的,相比jdk1.7而言,多了一個紅黑樹實現。當鏈表長度超過8的時候,就將鏈表變成紅黑樹,如圖所示。app

關於紅黑樹的實現,由於篇幅太長,在《集合系列》文章中紅黑樹設計,也有所介紹,這裏就不在詳細介紹了。函數

3、源碼解析

直接打開HashMap的源碼分析,能夠看到,主要有5個關鍵參數:源碼分析

  • threshold:表示容器所能容納的key-value對極限。
  • loadFactor:負載因子。
  • modCount:記錄修改次數。
  • size:表示實際存在的鍵值對數量。
  • table:一個哈希桶數組,鍵值對就存放在裏面。
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    
    //所能容納的key-value對極限
    int threshold;
    
    //負載因子
    final float loadFactor;
    
    //記錄修改次數
    int modCount;
    
    //實際存在的鍵值對數量
    int size;
    
    //哈希桶數組
    transient Node<K,V>[] table;
}

接着來看看Node這個類,NodeHashMap的一個內部類,實現了Map.Entry接口,本質是就是一個映射(鍵值對)

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//hash值
        final K key;//k鍵
        V value;//value值
        Node<K,V> next;//鏈表中下一個元素
}

在HashMap的數據結構中,有兩個參數能夠影響HashMap的性能:初始容量(inital capacity)負載因子(load factor)

初始容量(inital capacity)是指table的初始長度length(默認值是16);
負載因子(load factor)用指自動擴容的臨界值(默認值是0.75);

thresholdHashMap所能容納的最大數據量的Node(鍵值對)個數,計算公式threshold = capacity * Load factor。當entry的數量超過capacity*load_factor時,容器將自動擴容並從新哈希,擴容後的HashMap容量是以前容量的兩倍因此數組的長度老是2的n次方

初始容量負載因子也能夠修改,具體實現方式,能夠在對象初始化的時候,指定參數,好比:

Map map = new HashMap(int initialCapacity, float loadFactor);

可是,默認的負載因子0.75是對空間和時間效率的一個平衡選擇,建議你們不要修改,除非在時間和空間比較特殊的狀況下,若是內存空間不少而又對時間效率要求很高,能夠下降負載因子Load factor的值;相反,若是內存空間緊張而對時間效率要求不高,能夠增長負載因子loadFactor的值,這個值能夠大於1。 同時,對於插入元素較多的場景,能夠將初始容量設大,減小從新哈希的次數。

HashMap的內部功能實現有不少,本文主要從如下幾點,進行逐步分析。

  • 經過K獲取數組下標;
  • put方法的詳細執行;
  • resize擴容過程;
  • get方法獲取參數值;
  • remove刪除元素;

3.一、經過K獲取數組下標

無論增長、刪除仍是查找鍵值對,定位到數組的位置都是很關鍵的第一步,打開hashMap的任意一個增長、刪除、查找方法,從源碼能夠看出,經過key獲取數組下標,主要作了3步操做,其中length指的是容器數組的大小。

源碼部分:

/**獲取hash值方法*/
static final int hash(Object key) {
     int h;
     // h = key.hashCode() 爲第一步 取hashCode值(jdk1.7)
     // h ^ (h >>> 16)  爲第二步 高位參與運算(jdk1.7)
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//jdk1.8
}
/**獲取數組下標方法*/
static int indexFor(int h, int length) {
    //jdk1.7的源碼,jdk1.8沒有這個方法,可是實現原理同樣的
     return h & (length-1);  //第三步 取模運算
}

3.二、put方法的詳細執行

put(K key, V value)方法是將指定的key, value對添加到map裏。該方法首先會對map作一次查找,看是否包含該K,若是已經包含則直接返回;若是沒有找到,則將元素插入容器。具體插入過程以下:

具體執行步驟

  • 一、判斷鍵值對數組table[i]是否爲空或爲null,不然執行resize()進行擴容;
  • 二、根據鍵值key計算hash值獲得插入的數組索引i,若是table[i]==null,直接新建節點添加;
  • 三、當table[i]不爲空,判斷table[i]的首個元素是否和傳入的key同樣,若是相同直接覆蓋value;
  • 四、判斷table[i] 是否爲treeNode,即table[i] 是不是紅黑樹,若是是紅黑樹,則直接在樹中插入鍵值對;
  • 五、遍歷table[i],判斷鏈表長度是否大於8,大於8的話把鏈表轉換爲紅黑樹,在紅黑樹中執行插入操做,不然進行鏈表的插入操做;遍歷過程當中若發現key已經存在直接覆蓋value便可;
  • 六、插入成功後,判斷實際存在的鍵值對數量size是否超多了最大容量threshold,若是超過,進行擴容操做;

put方法源碼部分

/**
 * 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;
        //一、判斷數組table是否爲空或爲null
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //二、判斷數組下標table[i]==null
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //三、判斷table[i]的首個元素是否和傳入的key同樣
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //四、判斷table[i] 是否爲treeNode
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //五、遍歷table[i],判斷鏈表長度是否大於8
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //長度大於8,轉紅黑樹結構
                        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;
                }
            }
            //傳入的K元素已經存在,直接覆蓋value
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //六、判斷size是否超出最大容量
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
}

其中,與jdk1.7有區別的地方,第4步新增了紅黑樹插入方法,源碼部分:

/**
   * 紅黑樹的插入操做
   */
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                       int h, K k, V v) {
            Class<?> kc = null;
            boolean searched = false;
            TreeNode<K,V> root = (parent != null) ? root() : this;
            for (TreeNode<K,V> p = root;;) {
                //dir:遍歷的方向, ph:p節點的hash值
                int dir, ph; K pk;
                //紅黑樹是根據hash值來判斷大小
                // -1:左孩子方向 1:右孩子方向
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                //若是key存在的話就直接返回當前節點
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
                //若是當前插入的類型和正在比較的節點的Key是Comparable的話,就直接經過此接口比較
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0) {
                    if (!searched) {
                        TreeNode<K,V> q, ch;
                        searched = true;
                        //嘗試在p的左子樹或者右子樹中找到了目標元素
                        if (((ch = p.left) != null &&
                             (q = ch.find(h, k, kc)) != null) ||
                            ((ch = p.right) != null &&
                             (q = ch.find(h, k, kc)) != null))
                            return q;
                    }
                    //獲取遍歷的方向
                    dir = tieBreakOrder(k, pk);
                }
                //上面的全部if-else判斷都是在判斷下一次進行遍歷的方向,即dir
                TreeNode<K,V> xp = p;
                //當下面的if判斷進去以後就表明找到了目標操做元素,即xp
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    Node<K,V> xpn = xp.next;
                    //插入新的元素
                    TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    //由於TreeNode從此可能退化成鏈表,在這裏須要維護鏈表的next屬性
                    xp.next = x;
                    //完成節點插入操做
                    x.parent = x.prev = xp;
                    if (xpn != null)
                        ((TreeNode<K,V>)xpn).prev = x;
                    //插入操做完成以後就要進行必定的調整操做了
                    moveRootToFront(tab, balanceInsertion(root, x));
                    return null;
                }
       }
}

3.三、resize擴容過程

在說jdk1.8的HashMap動態擴容以前,咱們先來了解一下jdk1.7的HashMap擴容實現,由於jdk1.8代碼實現比Java1.7複雜了不止一倍,主要是Java1.8引入了紅黑樹設計,可是實現思想大同小異!

3.3.一、jdk1.7的擴容實現

源碼部分

/**
  * JDK1.7擴容方法
  * 傳入新的容量
  */
void resize(int newCapacity) {
    //引用擴容前的Entry數組
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    //擴容前的數組大小若是已經達到最大(2^30)了
    if (oldCapacity == MAXIMUM_CAPACITY) {
        //修改閾值爲int的最大值(2^31-1),這樣之後就不會擴容了
        threshold = Integer.MAX_VALUE;
        return;
    }
    //初始化一個新的Entry數組
    Entry[] newTable = new Entry[newCapacity];
    //將數據轉移到新的Entry數組裏,這裏包含最重要的從新定位
    transfer(newTable);
    //HashMap的table屬性引用新的Entry數組
    table = newTable;
    threshold = (int) (newCapacity * loadFactor);//修改閾值
}

transfer複製數組方法,源碼部分:

//遍歷每一個元素,按新的容量進行rehash,放到新的數組上
void transfer(Entry[] newTable) {
    //src引用了舊的Entry數組
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
        //遍歷舊的Entry數組
        Entry<K, V> e = src[j];
        //取得舊Entry數組的每一個元素
        if (e != null) {
            //釋放舊Entry數組的對象引用(for循環後,舊的Entry數組再也不引用任何對象)
            src[j] = null;
            do {
                Entry<K, V> next = e.next;
                //從新計算每一個元素在數組中的位置
                //實現邏輯,也是上文那個取模運算方法
                int i = indexFor(e.hash, newCapacity);
                //標記數組
                e.next = newTable[i];
                //將元素放在數組上
                newTable[i] = e;
                //訪問下一個Entry鏈上的元素,循環遍歷
                e = next;
            } while (e != null);
        }  
    }  
}

jdk1.7擴容總結:newTable[i]的引用賦給了e.next,也就是使用了單鏈表的頭插入方式,同一位置上新元素總會被放在鏈表的頭部位置;這樣先放在一個索引上的元素終會被放到Entry鏈的尾部(若是發生了hash衝突的話),這一點和Jdk1.8有區別。在舊數組中同一條Entry鏈上的元素,經過從新計算索引位置後,有可能被放到了新數組的不一樣位置上。

3.3.二、jdk1.8的擴容實現

源碼以下

final Node<K,V>[] resize() {
        //引用擴容前的node數組
        Node<K,V>[] oldTab = table;
        //舊的容量
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //舊的閾值
        int oldThr = threshold;
        //新的容量、閾值初始化爲0
        int newCap, newThr = 0;
        if (oldCap > 0) {
            //若是舊容量已經超過最大容量,讓閾值也等於最大容量,之後再也不擴容
                threshold = Integer.MAX_VALUE;
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 沒超過最大值,就擴充爲原來的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //若是舊容量翻倍沒有超過最大值,且舊容量不小於初始化容量16,則翻倍
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            //初始化容量設置爲閾值
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            //0的時候使用默認值初始化
            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;
                    //若是e沒有next節點,證實這個節點上沒有hash衝突,則直接把e的引用給到新的數組位置上
                    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 {
                        // 鏈表優化重hash的代碼塊
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        //從這條鏈表上第一個元素開始輪詢,若是當前元素新增的bit是0,則放在當前這條鏈表上,若是是1,則放在"j+oldcap"這個位置上,生成「低位」和「高位」兩個鏈表
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    //元素是不斷的加到尾部的,不會像1.7裏面同樣會倒序
                                    loTail.next = e;
                                //新增的元素永遠是尾元素
                                loTail = e;
                            }
                            else {
                                //高位的鏈表與低位的鏈表處理邏輯同樣,不斷的把元素加到鏈表尾部
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //低位鏈表放到j這個索引的位置上
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //高位鏈表放到(j+oldCap)這個索引的位置上
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
}

1.7與1.8處理邏輯大同小異,區別主要仍是在樹節點的分裂((TreeNode<K,V>)e).split()這個方法上

/**
 * 紅黑樹分裂方法
 */
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            //當前這個節點的引用,即這個索引上的樹的根節點
            TreeNode<K,V> b = this;
            // Relink into lo and hi lists, preserving order
            TreeNode<K,V> loHead = null, loTail = null;
            TreeNode<K,V> hiHead = null, hiTail = null;
            //高位低位的初始樹節點個數都設成0
            int lc = 0, hc = 0;
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
                next = (TreeNode<K,V>)e.next;
                e.next = null;
                //bit=oldcap,這裏判斷新bit位是0仍是1,若是是0就放在低位樹上,若是是1就放在高位樹上,這裏先是一個雙向鏈表
                if ((e.hash & bit) == 0) {
                    if ((e.prev = loTail) == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                    ++lc;
                }
                else {
                    if ((e.prev = hiTail) == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                    ++hc;
                }
            }

            if (loHead != null) {
                if (lc <= UNTREEIFY_THRESHOLD)
                    //!!!若是低位的鏈表長度小於閾值6,則把樹變成鏈表,並放到新數組中j索引位置
                    tab[index] = loHead.untreeify(map);
                else {
                    tab[index] = loHead;
                    //高位不爲空,進行紅黑樹轉換
                    if (hiHead != null) // (else is already treeified)
                        loHead.treeify(tab);
                }
            }
            if (hiHead != null) {
                if (hc <= UNTREEIFY_THRESHOLD)
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                    tab[index + bit] = hiHead;
                    if (loHead != null)
                        hiHead.treeify(tab);
                }
            }
}

untreeify方法,將樹轉變爲單向鏈表

/**
 * 將樹轉變爲單向鏈表
 */
final Node<K,V> untreeify(HashMap<K,V> map) {
            Node<K,V> hd = null, tl = null;
            for (Node<K,V> q = this; q != null; q = q.next) {
                Node<K,V> p = map.replacementNode(q, null);
                if (tl == null)
                    hd = p;
                else
                    tl.next = p;
                tl = p;
            }
            return hd;
}

treeify方法,將鏈表轉換爲紅黑樹,會根據紅黑樹特性進行顏色轉換、左旋、右旋等

/**
 * 鏈表轉換爲紅黑樹,會根據紅黑樹特性進行顏色轉換、左旋、右旋等
 */
final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                if (root == null) {
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    for (TreeNode<K,V> p = root;;) {
                        int dir, ph;
                        K pk = p.key;
                        if ((ph = p.hash) > h)
                            dir = -1;
                        else if (ph < h)
                            dir = 1;
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            dir = tieBreakOrder(k, pk);

                        TreeNode<K,V> xp = p;
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            x.parent = xp;
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;
                            //進行左旋、右旋調整
                            root = balanceInsertion(root, x);
                            break;
                        }
                    }
                }
            }
            moveRootToFront(tab, root);
}

jdk1.8在進行從新擴容以後,會從新計算hash值,由於n變爲2倍,假設初始 tableSize = 4 要擴容到 8 來講就是 0100 到 1000 的變化(左移一位就是 2 倍),在擴容中只用判斷原來的 hash 值與左移動的一位(newtable 的值)按位與操做是 0 或 1 就行,0 的話索引就不變,1 的話索引變成原索引 + oldCap;

其實現以下流程圖所示:

能夠看見,由於 hash 值原本就是隨機性的,因此 hash 按位與上 newTable 獲得的 0(擴容前的索引位置)和 1(擴容前索引位置加上擴容前數組長度的數值索引處)就是隨機的,因此擴容的過程就能把以前哈希衝突的元素再隨機的分佈到不一樣的索引去,這算是 JDK1.8 的一個優化點。

此外,JDK1.7中rehash的時候,舊鏈表遷移新鏈表的時候,若是在新表的數組索引位置相同,則鏈表元素會倒置,可是從上圖能夠看出,JDK1.8不會倒置。

同時,因爲 JDK1.7 中發生哈希衝突時僅僅採用了鏈表結構存儲衝突元素,因此擴容時僅僅是從新計算其存儲位置而已。而 JDK1.8 中爲了性能在同一索引處發生哈希衝突到必定程度時,鏈表結構會轉換爲紅黑數結構存儲衝突元素,故在擴容時若是當前索引中元素結構是紅黑樹且元素個數小於鏈表還原閾值時就會把樹形結構縮小或直接還原爲鏈表結構(其實現就是上面代碼片斷中的 split() 方法)。

3.四、get方法獲取參數值

get(Object key)方法根據指定的key值返回對應的value,getNode(hash(key), key))獲得相應的Node對象e,而後返回e.value。所以getNode()是算法的核心。

get方法源碼部分:

/**
  * JDK1.8 get方法
  * 經過key獲取參數值
  */
public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
}

經過hash值和key獲取節點Node方法,源碼部分:

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) {
            //一、判斷第一個元素是否與key匹配
            if (first.hash == hash &&
                ((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;
}

在紅黑樹中找到指定k的TreeNode,源碼部分:

/**
  * 這裏面狀況分不少中,主要是由於考慮了hash相同可是key值不一樣的狀況,查找的最核心仍是落在key值上
  */
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
            TreeNode<K,V> p = this;
            do {
                int ph, dir; K pk;
                TreeNode<K,V> pl = p.left, pr = p.right, q;
                //判斷要查詢元素的hash是否在樹的左邊
                if ((ph = p.hash) > h)
                    p = pl;
                //判斷要查詢元素的hash是否在樹的右邊
                else if (ph < h)
                    p = pr;
                //查詢元素的hash與當前樹節點hash相同狀況
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
                //上面的三步都是正常的在二叉查找樹中尋找對象的方法
                //若是hash相等,可是內容卻不相等
                else if (pl == null)
                    p = pr;
                else if (pr == null)
                    p = pl;
                 //若是能夠根據compareTo進行比較的話就根據compareTo進行比較
                else if ((kc != null ||
                          (kc = comparableClassFor(k)) != null) &&
                         (dir = compareComparables(kc, k, pk)) != 0)
                    p = (dir < 0) ? pl : pr;
                //根據compareTo的結果在右孩子上繼續查詢
                else if ((q = pr.find(h, k, kc)) != null)
                    return q;
                //根據compareTo的結果在左孩子上繼續查詢
                else
                    p = pl;
            } while (p != null);
            return null;
}

get方法,首先經過hash()函數獲得對應數組下標,而後依次判斷。

  • 一、判斷第一個元素與key是否匹配,若是匹配就返回參數值;
  • 二、判斷鏈表是否紅黑樹,若是是紅黑樹,就進入紅黑樹方法獲取參數值;
  • 三、若是不是紅黑樹結構,直接循環判斷,直到獲取參數爲止;

3.五、remove刪除元素

remove(Object key)的做用是刪除key值對應的Node,該方法的具體邏輯是在removeNode(hash(key), key, null, false, true)裏實現的。

remove方法,源碼部分:

/**
  * JDK1.8 remove方法
  * 經過key移除對象
  */
public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
}

經過key移除Node節點方法,源碼部分:

/**
  * 經過key移除Node節點
  */
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;
}

removeTreeNode移除紅黑樹節點方法,源碼部分:

final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                                  boolean movable) {
            int n;
            if (tab == null || (n = tab.length) == 0)
                return;
            int index = (n - 1) & hash;
            TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
            TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
            if (pred == null)
                tab[index] = first = succ;
            else
                pred.next = succ;
            if (succ != null)
                succ.prev = pred;
            if (first == null)
                return;
            if (root.parent != null)
                root = root.root();
            if (root == null || root.right == null ||
                (rl = root.left) == null || rl.left == null) {
                tab[index] = first.untreeify(map);  // too small
                return;
            }
            TreeNode<K,V> p = this, pl = left, pr = right, replacement;
            if (pl != null && pr != null) {
                TreeNode<K,V> s = pr, sl;
                while ((sl = s.left) != null) // find successor
                    s = sl;
                boolean c = s.red; s.red = p.red; p.red = c; // swap colors
                TreeNode<K,V> sr = s.right;
                TreeNode<K,V> pp = p.parent;
                if (s == pr) { // p was s's direct parent
                    p.parent = s;
                    s.right = p;
                }
                else {
                    TreeNode<K,V> sp = s.parent;
                    if ((p.parent = sp) != null) {
                        if (s == sp.left)
                            sp.left = p;
                        else
                            sp.right = p;
                    }
                    if ((s.right = pr) != null)
                        pr.parent = s;
                }
                p.left = null;
                if ((p.right = sr) != null)
                    sr.parent = p;
                if ((s.left = pl) != null)
                    pl.parent = s;
                if ((s.parent = pp) == null)
                    root = s;
                else if (p == pp.left)
                    pp.left = s;
                else
                    pp.right = s;
                if (sr != null)
                    replacement = sr;
                else
                    replacement = p;
            }
            else if (pl != null)
                replacement = pl;
            else if (pr != null)
                replacement = pr;
            else
                replacement = p;
            if (replacement != p) {
                TreeNode<K,V> pp = replacement.parent = p.parent;
                if (pp == null)
                    root = replacement;
                else if (p == pp.left)
                    pp.left = replacement;
                else
                    pp.right = replacement;
                p.left = p.right = p.parent = null;
            }
            //判斷是否須要進行紅黑樹結構調整
            TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);

            if (replacement == p) {  // detach
                TreeNode<K,V> pp = p.parent;
                p.parent = null;
                if (pp != null) {
                    if (p == pp.left)
                        pp.left = null;
                    else if (p == pp.right)
                        pp.right = null;
                }
            }
            if (movable)
                moveRootToFront(tab, r);
}

jdk1.8的刪除邏輯實現比較複雜,相比jdk1.7而言,多了紅黑樹節點刪除和調整:

  • 一、默認判斷鏈表第一個元素是不是要刪除的元素;
  • 二、若是第一個不是,就繼續判斷當前衝突鏈表是不是紅黑樹,若是是,就進入紅黑樹裏面去找;
  • 三、若是當前衝突鏈表不是紅黑樹,就直接在鏈表中循環判斷,直到找到爲止;
  • 四、將找到的節點,刪除掉,若是是紅黑樹結構,會進行顏色轉換、左旋、右旋調整,直到知足紅黑樹特性爲止;

4、總結

一、若是key是一個對象,記得在對象實體類裏面,要重寫equals和hashCode方法,否則在查詢的時候,沒法經過對象key來獲取參數值!
二、相比JDK1.7,JDK1.8引入紅黑樹設計,當鏈表長度大於8的時候,鏈表會轉化爲紅黑樹結構,發生衝突的鏈表若是很長,紅黑樹的實現很大程度優化了HashMap的性能,使查詢效率比JDK1.7要快一倍!
三、對於大數組的狀況,能夠提早給Map初始化一個容量,避免在插入的時候,頻繁的擴容,由於擴容自己就比較消耗性能!

5、參考

一、JDK1.7&JDK1.8 源碼
二、美團技術團隊 - Java 8系列之從新認識HashMap
三、簡書 - JDK1.8紅黑樹實現分析-此魚不得水
四、簡書 - JJDK 1.8 中 HashMap 擴容
五、Java HashMap 基礎面試常見問題

做者:炸雞可樂
出處:www.pzblog.cn

相關文章
相關標籤/搜索