1. 程式人生 > >java 集合類之HashMap

java 集合類之HashMap

本文從原始碼角度簡單介紹HashMap。

繼承架構

HashMap繼承結構

可以看出,HashMap主要是繼承了AbstractMap,實現了Map介面。類似的繼承了AbstractMap的還有ConcurrentHashMap,TreeMap等等

抽象資料模型

HashMap的抽象資料模型來自於演算法中查詢技術的雜湊演算法針對碰撞提出的拉鍊表。具體示意圖如下:拉鍊表
拉鍊法散列表
散列表是為了快速查詢而設計的一類資料結構,為了能快速定位到元素的位置,在儲存元素的時候使用雜湊演算法計算出鍵和儲存位置的對應關係,這樣在查詢元素的時候只需要按照鍵計算出儲存位置即可找到元素,不用遍歷查詢。但是不同的鍵可能會計算出相同的儲存位置,也就是發生了碰撞,此時就要解決碰撞,一種簡單的方法是發生碰撞之後查詢碰撞點之後的儲存空間是否空餘,依次往後儲存,這種方法相對簡單,但是也會加大碰撞的概率。另一種方法則是使用連結串列儲存hash相同的元素,就如同上面的示例一樣。

重要欄位

1 預設初始容量 16

     /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

2 最大容量 2^30

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
static final int MAXIMUM_CAPACITY = 1 << 30;

3 load factor

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

load factor 指的是resize的容量限制,load factor的選擇關係到HashMap的效能,如果太大,則碰撞會增多,查詢的開銷會更大。如果太小,resize太頻繁且空間利用率太低。使用0.75作為一個預設值是一個較為折衷的方案。

         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;

以上的程式碼片段摘自resize()函式,可以看到,在沒有指定初始容量的大多數情況下,初始的threshold等於 (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);而隨著resize的進行,threshold每次乘2,和capacity以相同的速度增長。一直都是(int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);這麼大。
4 轉為紅黑樹的閾值

/**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

這個jdk 8新增的優化,在1.7的時候,為了優化查詢,會使用尾插法來使得查詢最近插入的記錄變得更容易,1.8則使用紅黑樹來使連結串列很長時查詢更快速。在理想情況下,即key完全隨機分佈,HashMap每個桶上面的元素(即hash相同的key)的個數服從泊松分佈,依次計算出長度為0到8的概率如下:

     * 0:    0.60653066
     * 1:    0.30326533
     * 2:    0.07581633
     * 3:    0.01263606
     * 4:    0.00157952
     * 5:    0.00015795
     * 6:    0.00001316
     * 7:    0.00000094
     * 8:    0.00000006
     * more: less than 1 in ten million

說明長度大於8的鏈出現的概率很小,因此選用了8作為閾值

5 table陣列

    /**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
    transient Node<K,V>[] table;

這個field就是儲存資料的散列表。解釋一下為什麼這個Field是transient的,transient是使本field不參與序列化,如果注意到了,就會發現ArrayList的elements陣列,LinkedList的頭指標和尾指標也都是transient的,這是為什麼呢?如果是這樣的話豈不是都沒法序列化到真正的資料了嗎,其實不然,序列化的關鍵在於readObject()和writeObject(),實現了這兩個方法的類在反序列化/序列化的時候是直接呼叫這兩個方法而不是序列化域。解決了能否序列化的問題,那麼為什麼不直接序列化陣列呢,原因是key和bucketIndex的對應關係取決於hashCode()函式的實現,這個函式是一個native方法,在不同的平臺實現不同,舉例說明,加入key "abc"在windows平臺下hashCode()返回1 在linux平臺下返回2,那麼將key為abc的某個值放到第1個bucket之後傳送到linux平臺中反序列化,那麼按照key計算出來的bucketIndex是2,將會取不到正確的值。
6 entrySet

    /**
     * Holds cached entrySet(). Note that AbstractMap fields are used
     * for keySet() and values().
     */
    transient Set<Map.Entry<K,V>> entrySet;

這個是一個介面,提供給entrySet()抽象方法使用,但是需要注意的是,這個看起來像個集合類例項的field裡面並沒有儲存任何資料,其foreach和iterator方法都是對table的遍歷,而這一切的基礎都是Node是Map.Entry的實現類。除此之外,keySet(),values()這些方法也都是這樣實現的。

    public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    }
	final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
	// 只是返回了外部類的size,實際上都是對table field進行的操作
	        public final int size()                 { return size; }
	        public final void clear()               { HashMap.this.clear(); }
	        public final Iterator<Map.Entry<K,V>> iterator() {
	            return new EntryIterator();
	        }
	        public final boolean contains(Object o) {
	            if (!(o instanceof Map.Entry))
	                return false;
	            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
	            Object key = e.getKey();
	            Node<K,V> candidate = getNode(hash(key), key);
	            return candidate != null && candidate.equals(e);
	        }
	        public final boolean remove(Object o) {
	            if (o instanceof Map.Entry) {
	                Map.Entry<?,?> e = (Map.Entry<?,?>) o;
	                Object key = e.getKey();
	                Object value = e.getValue();
	                return removeNode(hash(key), key, value, true, true) != null;
	            }
	            return false;
	        }
	        public final Spliterator<Map.Entry<K,V>> spliterator() {
	            return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
	        }
	        public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
	            Node<K,V>[] tab;
	            if (action == null)
	                throw new NullPointerException();
	            if (size > 0 && (tab = table) != null) {
	                int mc = modCount;
	                for (int i = 0; i < tab.length; ++i) {
	                    for (Node<K,V> e = tab[i]; e != null; e = e.next)
	                        action.accept(e);
	                }
	                if (modCount != mc)
	                    throw new ConcurrentModificationException();
	            }
	        }
	    }

核心方法實現

1 put()
put(K,V)可以將一個鍵值對隱射放入table中

    // 暴露出去的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) {
         // 定義臨時變數處理,避免直接使用field繼續操作
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 假如尚未初始化,則開始初始化,注意呼叫預設建構函式沒有初始化,所以這個分支是經常執行的
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 假如該bucket沒有值,也就是沒有碰撞,那麼直接放入該bucket
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            // 假如發生了碰撞,記錄下現有的值
            Node<K,V> e; K k;
            // 判斷是隻有hash相同還是key也相同
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                // key相同,是更新操作
                e = p;
            else if (p instanceof TreeNode)
                // 僅僅是hash相同,是新增操作,而且當前的bucket鏈已經轉化為紅黑樹,生成一個新的結點
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
               // 是新增,而且不是紅黑樹
                for (int binCount = 0; ; ++binCount) {
               // 遍歷直到當前bucket 連結串列的末尾
                    if ((e = p.next) == null) {
                        // 尾插法插入資料
                        p.next = newNode(hash, key, value, null);
                        // 如果加上當前這個長度>= TREEIFY_THRESHOLD,那麼久轉為紅黑樹
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st,這裡是補上當前的這個
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 如果在連結串列裡面找到了key相同的元素,那麼記錄這個元素
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 前面將key已存在的所有情況裡,原來的元素都賦值給了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;
    }

2 resize()
擴容方法,在大小超過限制時擴容

    /**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     * 初始化或者將容量加倍。假如是空的,使用初始容量來建立儲存空間。否則,因為我們使用加倍的
     * 方法來擴容,那麼在擴容之後的新table裡,每個bin裡面的元素要麼還留在原來的位置,要麼移動到
     * 2的冪次
     * @return the table
     */
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        // 如果還沒初始化,oldCap為0 ,否則為舊錶的長度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        // 如果是擴容操作,不是初始化操作
        if (oldCap > 0) {
        // 如果已經到了容量限制,那麼不再擴容,只是擴大threshold,使得不再出發擴容
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 如果沒到容量限制,而且舊錶的大小大於預設大小,那麼將容量和threshold加倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        // 如果沒有初始化,但是設定了初始容量,就將容量設定為threshold,這個threshold是通過
        // tableSizeFor()的函式算來的,這個函式可以計算出比當前數大的最小2的n次冪
        // 如tablseSizeFor(10)=16,tableSizeFor(3)=4
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        // 假如是使用了無參建構函式之後的初始化,那麼直接設定容量為初始容量
        // 設定threshold為load factor* capacity
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 如果是設定了初始容量之後的初始化,那麼設定一下threshold
        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;
                // 如果這個bucket不是空
                if ((e = oldTab[j]) != null) {
                    // 設定舊的為空,避免記憶體洩漏
                    oldTab[j] = null;
                    // 如果沒有鏈就直接複製過去
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    // 如果是個紅黑樹節點,那麼劃分為low樹和high樹
                    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;
                            // 注意這個地方的e.hash & oldCap只能是0或oldCap,因為oldCap是2的整數次冪,
                            // 也就是2進位制只有一個1,這樣可以隨機將連結串列分為兩個部分
                            if ((e.hash & oldCap) == 0) {
                            // 將所有(e.hash & oldCap) == 0的放到低表裡
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                            // 將所有(e.hash & oldCap) == oldCap的放到高表裡
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 低表的資料不變
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        // 高表的資料往後移oldCap個單位
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
/**
* 劃分紅黑樹的演算法
*/
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;
            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;
                // 將hash & capacity ==0 的放入loHead的連結串列中
                if ((e.hash & bit) == 0) {
                    if ((e.prev = loTail) == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                    ++lc;
                }
                // 將hash & capacity !=0 的放入hiTail的連結串列中
                else {
                    if ((e.prev = hiTail) == null)
                        hiHead = e;
                    else