1. 程式人生 > >Java集合框架之Map---HashMap和LinkedHashMap原始碼分析

Java集合框架之Map---HashMap和LinkedHashMap原始碼分析


1、HashMap概述:

   HashMap是基於雜湊表的Map介面的非同步實現。此實現提供所有可選的對映操作,並允許使用null值和null鍵。此類不保證對映的順序,特別是它不保證該順序恆久不變。

2、HashMap的資料結構

資料結構中有陣列和連結串列來實現對資料的儲存,但這兩者基本上是兩個極端。所有的資料結構都可以用這兩個基本結構來構造的
陣列:陣列儲存區間是連續的,佔用記憶體嚴重,故空間複雜的很大。但陣列的二分查詢時間複雜度小,為O(1);陣列的特點是:定址容易,插入和刪除困難
連結串列:連結串列儲存區間離散,佔用記憶體比較寬鬆,故空間複雜度很小,但時間複雜度很大,達O(N)。連結串列的特點是:定址困難,插入和刪除容易

雜湊表((Hash table):由陣列+連結串列組成的。既滿足了資料的查詢方便,同時不佔用太多的內容空間,使用也十分方便。
  雜湊表有多種不同的實現方法,我接下來解釋的是最常用的一種方法—— 拉鍊法,我們可以理解為“連結串列的陣列”

一個長度為16的陣列中,每個元素儲存的是一個連結串列的頭結點。那麼這些元素是按照什麼樣的規則儲存到陣列中呢。一般情況是通過hash(key)%len獲得,也就是元素的key的雜湊值對陣列長度取模得到。比如上述雜湊表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都儲存在陣列下標為12的位置。

  HashMap其實也是一個線性的陣列實現的,所以可以理解為其儲存資料的容器就是一個線性陣列。這可能讓我們很不解,一個線性的陣列怎麼實現按鍵值對來存取資料呢?這裡HashMap有做一些處理。

  首先HashMap裡面實現一個靜態內部類Entry,其重要的屬性有 key , value, next,從屬性key,value我們就能很明顯的看出來Entry就是HashMap鍵值對實現的一個基礎bean,我們上面說到HashMap的基礎就是一個線性陣列,這個陣列就是Entry[],Map裡面的內容都儲存在Entry[]裡面。其中final修飾的方法是實現介面的方法,使用final的原因有兩個。第一個原因是把方法鎖定,以防任何繼承類修改它的含義;第二個原因是效率

    static final Entry<?,?>[] EMPTY_TABLE = {};
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
   /** Entry是單向連結串列。    
     * 它是 “HashMap鏈式儲存法”對應的連結串列。    
     *它實現了Map.Entry 介面,即實現getKey(), getValue(), setValue(V value), equals(Object o), hashCode()這些函式  
    **/  
    static class Entry<K,V> implements Map.Entry<K,V> {    
        final K key;    
        V value;    
        // 指向下一個節點    
        Entry<K,V> next;    
        final int hash;    
   
        // 建構函式。    
        // 輸入引數包括"雜湊值(h)", "鍵(k)", "值(v)", "下一節點(n)"    
        Entry(int h, K k, V v, Entry<K,V> n) {    
            value = v;    
            next = n;    
            key = k;    
            hash = h;    
        }    
   
        public final K getKey() {    
            return key;    
        }    
   
        public final V getValue() {    
            return value;    
        }    
   
        public final V setValue(V newValue) {    
            V oldValue = value;    
            value = newValue;    
            return oldValue;    
        }    
   
        // 判斷兩個Entry是否相等    
        // 若兩個Entry的“key”和“value”都相等,則返回true。    
        // 否則,返回false    
        public final boolean equals(Object o) {    
            if (!(o instanceof Map.Entry))    
                return false;    
            Map.Entry e = (Map.Entry)o;    
            Object k1 = getKey();    
            Object k2 = e.getKey();    
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {    
                Object v1 = getValue();    
                Object v2 = e.getValue();    
                if (v1 == v2 || (v1 != null && v1.equals(v2)))    
                    return true;    
            }    
            return false;    
        }    
   
        // 實現hashCode()    
        public final int hashCode() {    
            return (key==null   ? 0 : key.hashCode()) ^    
                   (value==null ? 0 : value.hashCode());    
        }    
   
        public final String toString() {    
            return getKey() + "=" + getValue();    
        }    
   
        // 當向HashMap中新增元素時,繪呼叫recordAccess()。    
        // 這裡不做任何處理    
        void recordAccess(HashMap<K,V> m) {    
        }    
   
        // 當從HashMap中刪除元素時,繪呼叫recordRemoval()。    
        // 這裡不做任何處理    
        void recordRemoval(HashMap<K,V> m) {    
        }    
    }

3、HashMap的存取實現

3.1:存資料

put函式大致的思路為:     對key的hashCode()做hash,然後再計算index;     如果沒碰撞直接放到bucket裡;     如果碰撞了,以連結串列的形式存在buckets後;     如果碰撞導致連結串列過長(大於等於TREEIFY_THRESHOLD),就把連結串列轉換成紅黑樹(java8);     如果節點已經存在就替換old value(保證key的唯一性)      如果bucket滿了(超過load factor*current capacity),就要resize。

從下面的原始碼中可以看出:當我們往HashMap中put元素的時候,先根據key的hashCode重新計算hash值,根據hash值得到這個元素在陣列中的位置(即下標),如果陣列該位置上已經存放有其他元素了,那麼在這個位置上的元素將以連結串列的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。如果陣列該位置上沒有元素,就直接將該元素放到此陣列中的該位置上。

        public V put(K key, V value) {

            // HashMap允許存放null鍵和null值。  
            // 當key為null時,呼叫putForNullKey方法,將value放置在陣列第一個位置。  
            if (key == null)
                return putForNullKey(value);
            
            // 根據key的keyCode重新計算hash值。  
            int hash = hash(key);
            
            // 搜尋指定hash值在對應table中的索引。  
            int i = indexFor(hash, table.length);
            
            // 如果 i 索引處的 Entry 不為 null,通過迴圈不斷遍歷 e 元素的下一個元素。  
            for (Entry<K,V> e = table[i]; e != null; e = e.next) {
                Object k;
                if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                    V oldValue = e.value;
                    e.value = value;
                    e.recordAccess(this);
                    return oldValue;
                }
            }
            // 如果i索引處的Entry為null,表明此處還沒有Entry。  
            modCount++;
            // 將key、value新增到i索引處。  
            addEntry(hash, key, value, i);
            return null;
        }

putForNullKey(V value)方法讓HashMap的key可以為空

     private V putForNullKey(V value) {
        // 當key為null時,將value放置在陣列第一個位置。
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value; 
                e.recordAccess(this);//這個方法在HashMap裡是空方法,LinkedHashMap有重寫
                return oldValue;
            }
        }

        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

   addEntry(hash, key, value, i)方法根據計算出的hash值,將key-value對放在陣列table的i索引處。addEntry 是 HashMap 提供的一個包訪問許可權的方法,程式碼如下:

當系統決定儲存HashMap中的key-value對時,完全沒有考慮Entry中的value,僅僅只是根據key來計算並決定每個Entry的儲存位置。我們完全可以把 Map 集合中的 value 當成 key 的附屬,當系統決定了 key 的儲存位置之後,value 隨之儲存在那裡即可。

    void addEntry(int hash, K key, V value, int bucketIndex) {
        // 如果 Map 中的 key-value 對的數量超過了極限
        if ((size >= threshold) && (null != table[bucketIndex])) {
            // 把 table 物件的長度擴充到原來的2倍
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

    void createEntry(int hash, K key, V value, int bucketIndex) {
        // 獲取指定 bucketIndex 索引處的 Entry 
        Entry<K,V> e = table[bucketIndex];

        // 將新建立的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }
   hash(int h)方法根據keyhashCode重新計算一次雜湊。此演算法加入了高位計算,防止低位不變,高位變化時,造成的hash衝突。
    final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();//hashCode()方法只在這裡被呼叫過

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
   我們可以看到在HashMap中要找到某個元素,需要根據key的hash值來求得對應陣列中的位置。如何計算這個位置就是hash演算法。前面說過HashMap的資料結構是陣列和連結串列的結合,所以我們當然希望這個HashMap裡面的 元素位置儘量的分佈均勻些,儘量使得每個位置上的元素數量只有一個,那麼當我們用hash演算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們要的,而不用再去遍歷連結串列,這樣就大大優化了查詢的效率。

   對於任意給定的物件,只要它的 hashCode() 返回值相同,那麼程式呼叫 hash(int h) 方法所計算得到的 hash 碼值總是相同的。我們首先想到的就是把hash值對陣列長度取模運算,這樣一來,元素的分佈相對來說是比較均勻的。但是,“模”運算的消耗還是比較大的,在HashMap中是這樣做的:呼叫 indexFor(int h, int length) 方法來計算該物件應該儲存在 table 陣列的哪個索引處。indexFor(int h, int length) 方法的程式碼如下:
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }
   這個方法非常巧妙,它通過 h & (table.length -1) 來得到該物件的儲存位,而HashMap底層陣列的長度總是 2 的 n 次方,這是HashMap在速度上的優化。在 HashMap 構造器中有如下程式碼:
   這段程式碼保證初始化時HashMap的容量總是2的n次方,即底層陣列的長度總是為2的n次方。
當length總是 2 的n次方時,h& (length-1)運算等價於對length取模,也就是h%length,但是&比%具有更高的效率。

   這看上去很簡單,其實比較有玄機的,我們舉個例子來說明:
   假設陣列長度分別為15和16,優化後的hash碼分別為8和9,那麼&運算後的結果如下:

       h & (table.length-1)                     hash                             table.length-1

       8 & (15-1):                                 0100                   &              1110                   =                0100

       9 & (15-1):                                 0101                   &              1110                   =                0100
       -----------------------------------------------------------------------------------------------------------------------

       8 & (16-1):                                 0100                   &              1111                   =                0100

       9 & (16-1):                                 0101                   &              1111                   =                0101
 
   從上面的例子中可以看出:當它們和15-1(1110)“與”的時候,產生了相同的結果,也就是說它們會定位到陣列中的同一個位置上去,這就產生了碰撞,8和9會被放到陣列中的同一個位置上形成連結串列,那麼查詢的時候就需要遍歷這個鏈 表,得到8或者9,這樣就降低了查詢的效率。同時,我們也可以發現,當陣列長度為15的時候,hash值會與15-1(1110)進行“與”,那麼 最後一位永遠是0,而0001,0011,0101,1001,1011,0111,1101這幾個位置永遠都不能存放元素了,空間浪費相當大,更糟的是這種情況中,陣列可以使用的位置比陣列長度小了很多,這意味著進一步增加了碰撞的機率,減慢了查詢的效率!而當陣列長度為16時,即為2的n次方時,2n-1得到的二進位制數的每個位上的值都為1,這使得在低位上&時,得到的和原hash的低位相同,加之hash(int h)方法對key的hashCode的進一步優化,加入了高位計算,就使得只有相同的hash值的兩個值才會被放到陣列中的同一個位置上形成連結串列。

   所以說,當陣列長度為2的n次冪的時候,不同的key算得得index相同的機率較小,那麼資料在陣列上分佈就比較均勻,也就是說碰撞的機率小,相對的,查詢的時候就不用遍歷某個位置上的連結串列,這樣查詢效率也就較高了。

   根據上面 put 方法的原始碼可以看出,當程式試圖將一個key-value對放入HashMap中時,程式首先根據該 key 的 hashCode() 返回值決定該 Entry 的儲存位置:如果兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的儲存位置相同。如果這兩個 Entry 的 key 通過 equals 比較返回 true,新新增 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但key不會覆蓋。如果這兩個 Entry 的 key 通過 equals 比較返回 false,新新增的 Entry 將與集合中原有 Entry 形成 Entry 鏈,而且新新增的 Entry 位於 Entry 鏈的頭部——具體說明繼續看 addEntry() 方法的說明。

3.2:讀取資料

在理解了put之後,get就很簡單了。大致思路如下:

    bucket裡的第一個節點,直接命中;
    如果有衝突,則通過key.equals(k)去查詢對應的entry
    若為樹,則在樹中通過key.equals(k)查詢,O(logn);
    若為連結串列,則在連結串列中通過key.equals(k)查詢,O(n)。

有了上面儲存時的hash演算法作為基礎,理解起來這段程式碼就很容易了。從下面的原始碼中可以看出:從HashMapget元素時,首先計算keyhashCode,找到陣列中對應位置的某一元素,然後通過keyequals方法在對應位置的連結串列中找到需要的元素。

    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }
   /**
     * The number of key-value mappings contained in this map.
     */
    transient int size;

private V getForNullKey() { if (size == 0) { return null; } for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }

首先計算keyhashCode,找到陣列中對應位置的某一元素,然後通過keyequals方法在對應位置的連結串列中找到需要的元素。

    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

歸納起來簡單地說,HashMap 在底層將 key-value 當成一個整體進行處理,這個整體就是一個 Entry 物件。HashMap 底層採用一個 Entry[] 陣列來儲存所有的 key-value 對,當需要儲存一個 Entry 物件時,會根據hash演算法來決定其在陣列中的儲存位置,在根據equals方法決定其在該陣列位置上的連結串列中的儲存位置;當需要取出一個Entry時,也會根據hash演算法找到其在陣列中的儲存位置,再根據equals方法從該位置上的連結串列中取出該Entry。

4、HashMap的resize(rehash):

   當HashMap中的元素越來越多的時候,hash衝突的機率也就越來越高,因為陣列的長度是固定的。所以為了提高查詢的效率,就要對HashMap的陣列進行擴容,陣列擴容這個操作也會出現在ArrayList中,這是一個常用的操作,而在HashMap陣列擴容之後,最消耗效能的點就出現了:原陣列中的資料必須重新計算其在新陣列中的位置,並放進去,這就是resize。

   那麼HashMap什麼時候進行擴容呢?當HashMap中的元素個數超過陣列大小*loadFactor時,就會進行陣列擴容,loadFactor的預設值為0.75,這是一個折中的取值。也就是說,預設情況下,陣列大小為16,那麼當HashMap中元素個數超過16*0.75=12的時候,就把陣列的大小擴充套件為 2*16=32,即擴大一倍,然後重新計算每個元素在陣列中的位置,而這是一個非常消耗效能的操作,所以如果我們已經預知HashMap中元素的個數,那麼預設元素的個數能夠有效的提高HashMap的效能。

    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

5、 HashMap的效能引數:

   HashMap 包含如下幾個構造器:
   HashMap():構建一個初始容量為 16,負載因子為 0.75 的 HashMap。
   HashMap(int initialCapacity):構建一個初始容量為 initialCapacity,負載因子為 0.75 的 HashMap。
   HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的負載因子建立一個 HashMap。

   HashMap的基礎構造器HashMap(int initialCapacity, float loadFactor)帶有兩個引數,它們是初始容量initialCapacity和載入因子loadFactor。
   initialCapacity:HashMap的最大容量,即為底層陣列的長度。
   loadFactor:負載因子loadFactor定義為:散列表的實際元素數目(n)/ 散列表的容量(m)。
   負載因子衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。對於使用連結串列法的散列表來說,查詢一個元素的平均時間是O(1+a),因此如果負載因子越大,對空間的利用更充分,然而後果是查詢效率的降低;如果負載因子太小,那麼散列表的資料將過於稀疏,對空間造成嚴重浪費。

   HashMap的實現中,通過threshold欄位來判斷HashMap的最大容量:

threshold = (int)(capacity * loadFactor); 
   結合負載因子的定義公式可知,threshold就是在此loadFactor和capacity對應下允許的最大元素數目,超過這個數目就重新resize,以降低實際的負載因子。預設的的負載因子0.75是對空間和時間效率的一個平衡選擇。當容量超出此最大容量時, resize後的HashMap容量是容量的兩倍:
if (size++ >= threshold)     
    resize(2 * table.length);  

6、Fail-Fast機制:

   我們知道java.util.HashMap不是執行緒安全的,因此如果在使用迭代器的過程中有其他執行緒修改了map,那麼將丟擲ConcurrentModificationException,這就是所謂fail-fast策略
   這一策略在原始碼中的實現是通過modCount域,modCount顧名思義就是修改次數,對HashMap內容的修改都將增加這個值,那麼在迭代器初始化過程中會將這個值賦給迭代器的expectedModCount。

        HashIterator() {
            expectedModCount = modCount;
            if (size > 0) { // advance to first entry
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
        }
在迭代過程中,判斷modCountexpectedModCount是否相等,如果不相等就表示已經有其他執行緒修改了Map:

   注意到modCount宣告為volatile,保證執行緒之間修改的可見性。

    final Entry<K,V> nextEntry() {     
        if (modCount != expectedModCount)     
            throw new ConcurrentModificationException();  
   在HashMap的API中指出:

   由所有HashMap類的“collection 檢視方法”所返回的迭代器都是快速失敗的:在迭代器建立之後,如果從結構上對對映進行修改,除非通過迭代器本身的 remove 方法,其他任何時間任何方式的修改,迭代器都將丟擲 ConcurrentModificationException。因此,面對併發的修改,迭代器很快就會完全失敗,而不冒在將來不確定的時間發生任意不確定行為的風險。

   注意,迭代器的快速失敗行為不能得到保證,一般來說,存在非同步的併發修改時,不可能作出任何堅決的保證。快速失敗迭代器盡最大努力丟擲 ConcurrentModificationException。因此,編寫依賴於此異常的程式的做法是錯誤的,正確做法是:迭代器的快速失敗行為應該僅用於檢測程式錯誤。

1. 什麼時候會使用HashMap?他有什麼特點?
是基於Map介面的實現,儲存鍵值對時,它可以接收null的鍵值,是非同步的,HashMap儲存著Entry(hash, key, value, next)物件。

2. 你知道HashMap的工作原理嗎?

通過hash的方法,通過put和get儲存和獲取物件。儲存物件時,我們將K/V傳給put方法時,它呼叫hashCode計算hash從而得到bucket位置,進一步儲存,HashMap會根據當前bucket的佔用情況自動調整容量(超過Load Facotr則resize為原來的2倍)。獲取物件時,我們將K傳給get,它呼叫hashCode計算hash從而得到bucket位置,並進一步呼叫equals()方法確定鍵值對。如果發生碰撞的時候,Hashmap通過連結串列將產生碰撞衝突的元素組織起來,在Java 8中,如果一個bucket中碰撞衝突的元素超過某個限制(預設是8),則使用紅黑樹來替換連結串列,從而提高速度。

3. 你知道get和put的原理嗎?equals()和hashCode()的都有什麼作用?

通過對key的hashCode()進行hashing,並計算下標( n-1 & hash),從而獲得buckets的位置。如果產生碰撞,則利用key.equals()方法去連結串列或樹中去查詢對應的節點

4. 你知道hash的實現嗎?為什麼要這樣實現?
在Java 1.8的實現中,是通過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質量來考慮的,這麼做可以在bucket的n比較小的時候,也能保證考慮到高低bit都參與到hash的計算中,同時不會有太大的開銷。

5. 如果HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?

如果超過了負載因子(預設0.75),則會重新resize一個原來長度兩倍的HashMap,並且重新呼叫hash方法。

二、LinkedHashMap

    LinkedHashMap繼承自HashMap,所以擁有HashMap的大部分特性,比如支援null鍵和值,預設容量為16,裝載因子為0.75,非執行緒安全等等。一個有序的Map介面實現,這裡的有序指的是元素可以按插入順序或訪問順序排列;
    與HashMap的異同:同樣是基於散列表實現,區別是,LinkedHashMap內部多了一個雙向迴圈連結串列的維護,該連結串列是有序的,可以按元素插入順序或元素最近訪問順序(LRU)排列,簡單地說:LinkedHashMap=散列表+迴圈雙向連結串列
LinkedHashMap的陣列結構
  用畫圖工具簡單畫了下散列表和迴圈雙向連結串列,如下圖,簡單說明下:
    第一張圖是LinkedHashMap的全部資料結構,包含散列表和迴圈雙向連結串列,由於迴圈雙向連結串列線條太多了,不好畫,簡單的畫了一個節點(黃色圈出來的)示意一下,注意左邊的紅色箭頭引用為Entry節點物件的next引用(散列表中的單鏈表),綠色線條為Entry節點物件的before, after引用(迴圈雙向連結串列的前後引用);


    第二張圖專門把迴圈雙向連結串列抽取出來,直觀一點,注意該迴圈雙向連結串列的頭部存放的是最久訪問的節點或最先插入的節點,尾部為最近訪問的或最近插入的節點,迭代器遍歷方向是從連結串列的頭部開始到連結串列尾部結束,在連結串列尾部有一個空的header節點,該節點不存放key-value內容,為LinkedHashMap類的成員屬性,迴圈雙向連結串列的入口;


1、原始碼分析

1.屬性
  LinkedHashMap只定義了兩個屬性:其中header代表內部雙向連結串列的頭結點,後面我們就會發現,LinkedHashMap除了有個桶陣列容納所有Entry之外,還有一個雙向連結串列儲存所有Entry引用。遍歷的時候,並不是去遍歷桶陣列,而是直接遍歷雙向連結串列所以LinkedHashMap的遍歷時間不受桶容量的限制,這是它和HashMap的重要區別之一。
public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
{

    private static final long serialVersionUID = 3801124242820219131L;

    /**
     * 雙向迴圈連結串列,  頭結點(空節點)
     */
    private transient Entry<K,V> header;

    /**
     * accessOrder為true時,按訪問順序排序,false代表按照插入順序排序,true表示訪問順序
     */
    private final boolean accessOrder;
    ......
}
2.構造方法

如果要設定訪問順序為true,同時也要設定容量大小和負載因子。

    /**
     * 生成一個空的LinkedHashMap,並指定其容量大小和負載因子,
     * 預設將accessOrder設為false,按插入順序排序
     */
    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }

    /**
     * 生成一個空的LinkedHashMap,並指定其容量大小,負載因子使用預設的0.75,
     * 預設將accessOrder設為false,按插入順序排序
     */
    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }

    /**
     * 生成一個空的HashMap,容量大小使用預設值16,負載因子使用預設值0.75
     * 預設將accessOrder設為false,按插入順序排序.
     */
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }

    /**
     * 根據指定的map生成一個新的HashMap,負載因子使用預設值,初始容量大小為Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,DEFAULT_INITIAL_CAPACITY)
     * 預設將accessOrder設為false,按插入順序排序.
     */
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super(m);
        accessOrder = false;
    }

    /**
     * 生成一個空的LinkedHashMap,並指定其容量大小和負載因子,
     * 預設將accessOrder設為true,按訪問順序排序
     */
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }
  從構造方法中可以看出,預設都採用插入順序來維持取出鍵值對的次序。所有構造方法都是通過呼叫父類的構造方法來建立物件的。
  LinkedHashMap是基於雙向連結串列的,而且屬性中定了一個header節點,為什麼構造方法都沒有對其進行初始化呢?
     注意LinkedHashMap中有一個init()方法, HashMap的構造方法都呼叫了init()方法,這裡LinkedHashMap的構造方法在呼叫父類構造方法後將從父類構造方法中呼叫init()方法(這也解釋了為什麼HashMap中會有一個沒有內容的init()方法)。這不光是個雙向連結串列,還是個迴圈連結串列。
    /**
     * 覆蓋HashMap的init方法,在構造方法、Clone、readObject方法裡會呼叫該方法
     * 作用是生成一個雙向連結串列頭節點,初始化其前後節點引用
     */
    @Override
    void init() {
        header = new Entry<>(-1, null, null, null);//初始化雙向連結串列
        header.before = header.after = header;
    }
HashMap的構造方法
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        this.loadFactor = loadFactor;
        threshold = initialCapacity;
        init();
    }
     HashMap構造器最後一步呼叫了一個init方法,而這個init方法在HashMap中是個空實現,沒有任何程式碼。 這其實就是所謂的“鉤子”,具體程式碼由子類實現,如果子類希望每次構造的時候都去做一些特定的初始化操作,可以選擇複寫init方法。我們看到LinkedHashMap中確實複寫了init:

transfer(HashMap.Entry[] newTable)方法在HashMap呼叫resize(int newCapacity)方法的時候被呼叫。

    /**
     * 覆蓋HashMap的transfer方法,效能優化,這裡遍歷方式不採用HashMap的雙重迴圈方式
     * 而是直接通過雙向連結串列遍歷Map中的所有key-value對映
     */
    @Override
    void transfer(HashMap.Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        //遍歷舊Map中的所有key-value
        for (Entry<K,V> e = header.after; e != header; e = e.after) {
            if (rehash)
                e.hash = (e.key == null) ? 0 : hash(e.key);
            //根據新的陣列長度,重新計算索引,
            int index = indexFor(e.hash, newCapacity);
            //插入到連結串列表頭
            e.next = newTable[index];
            //將e放到索引為i的陣列處
            newTable[index] = e;
        }
    }

----------------------------------------------------------------------------------
HashMap內部的Entry類並沒有before和after指標, 也就是說LinkedHashMap自己重寫了一個Entry類

   /**
     * LinkedHashMap節點物件
     */
    private static class Entry<K,V> extends HashMap.Entry<K,V> {
        // 節點前後引用
        Entry<K,V> before, after;

        //建構函式與HashMap一致
        Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
            super(hash, key, value, next);
        }

        //LinkedHashMap沒有重寫remove(Object key)方法,重寫了被remove呼叫的recordRemoval方法,這個方法在HashMap裡是空方法
        //這個方法的設計也和精髓,也是模板方法模式
        //HahsMap remove(Object key)把資料從橫向陣列 * 豎向next連結串列裡面移除之後(就已經完成工作了,所以HashMap裡面recordRemoval是空的實現呼叫了此方法
        //但在LinkedHashMap裡面,還需要移除header連結串列裡面Entry的after和before關係
        void recordRemoval(HashMap<K,V> m) {
            remove();
        }

        /**
         * 移除節點,並修改前後引用
         */
        private void remove() {
            before.after = after;
            after.before = before;
        }

        /**
         * 將當前節點插入到existingEntry的前面
         */
        private void addBefore(Entry<K,V> existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }

        /**
         * 在HashMap的put和get方法中,會呼叫該方法,在HashMap中該方法為空
         * 在LinkedHashMap中,當按訪問順序排序時,該方法會將當前節點插入到連結串列尾部(頭結點的前一個節點),否則不做任何事
         */
        void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            //當LinkedHashMap按訪問排序時
            if (lm.accessOrder) {
                lm.modCount++;
                //移除當前節點
                remove();
                //將當前節點插入到頭結點前面
                addBefore(lm.header);
            }
        }

    }
     這裡的Entry選擇繼承父類的Entry類,也就是說 LinkedHashMap中的Entry擁有三個指標,除了前驅後繼指標外用於雙向連結串列的連線外,還有一個next指標用於解決hash衝突(引用鏈)。    除此之外,Entry新增了幾個方法,remove和addbefore用來操作雙向連結串列不用多說。而recordAccess方法比較特殊,這個方法在HashMap中也是空實現,在HashMapput和get方法中,會呼叫該方法,在LinkedHashMap的get方法中也會呼叫 。也就是說,只要涉及到訪問結點,那麼就會呼叫這個方法。觀察該方法的邏輯: 如果accessOrder為true,那麼會呼叫addBefore方法將當前Entry放到雙向連結串列的尾部,最終在我們遍歷連結串列的時候就會發現最近最少使用的結點的都集中在連結串列頭部( 從近期訪問最少到近期訪問最多的順序),這就是LRU。   (recordAccess方法解釋:當呼叫此類的get方法或put方法(put方法將呼叫到父類HashMap.Entry的put 方法)都將呼叫到recordAccess(HashMap<K,V> m)方法, 如果accessOrder為true,即使用的是最近最少使用的次序,則將當前被修改的,節點移動到header節點之前,即連結串列的尾部。這也是為什麼在HashMap.Entry中有一個空的recordAccess(HashMap<K,V> m)方法的原因)
    /**
     * 通過key獲取value,與HashMap的區別是:當LinkedHashMap按訪問順序排序的時候,會將訪問的當前節點移到連結串列尾部(頭結點的前一個節點)
     */
    public V get(Object key) {
        Entry<K,V> e = (Entry<K,V>)getEntry(key);
        if (e == null)
            return null;
        e.recordAccess(this);
        return e.value;
    }
-------------------------------------------------------------------------------------------
    LinkedHashMap並沒有複寫put方法,但是卻重寫了addEntry和createEntry方法,之前分析HashMap的時候我們就知道了,put方法會呼叫addEntry將鍵值對掛到桶的某個合適位置,而addEntry又會呼叫createEntry方法建立一個鍵值對物件。因而,LinkedHashMap其實是間接更改了put方法,想想也很容易理解,LinkedHashMap除了要向桶中新增鍵值對外,還需向連結串列中增加鍵值對,所以必須得修改put方法。
   /**
     * 建立節點,插入到LinkedHashMap中,該方法覆蓋HashMap的addEntry方法
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {
        super.addEntry(hash, key, value, bucketIndex);

        // 注意頭結點的下個節點即header.after,存放於連結串列頭部,是最不經常訪問或第一個插入的節點,
        //有必要的情況下(如容量不夠,具體看removeEldestEntry方法的實現,這裡預設為false,不刪除),可以先刪除
        Entry<K,V> eldest = header.after;
        if (removeEldestEntry(eldest)) {
            removeEntryForKey(eldest.key);
        }
    }

    /**
     * 建立節點,並將該節點插入到連結串列尾部
     */
    void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMap.Entry<K,V> old = table[bucketIndex];
        Entry<K,V> e = new Entry<>(hash, key, value, old);
        table[bucketIndex] = e;
        //將該節點插入到連結串列尾部
        e.addBefore(header);
        size++;
    }
createEntry方法會將鍵值對分別掛到桶陣列和雙向連結串列中。
  比較有意思的是addEntry方法,它提供了一個可選的操作,我們可以通過繼承LinkedHashMap並複寫removeEldestEntry方法讓該子類可以自動地刪除最近最少訪問的鍵值對——這可以用來做快取!!
    /**
     * 該方法在建立新節點的時候呼叫,
     * 判斷是否有必要刪除連結串列頭部的第一個節點(最不經常訪問或最先插入的節點,由accessOrder決定)
     */
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }
為什麼這個方法始終返回false?
   結合上面的addEntry(int hash,K key,V value,int bucketIndex)方法,這樣設計可以使LinkedHashMap成為一個正常的Map,不會去移除“最老”的節點。
為什麼不在程式碼中直接去除這部分邏輯而是設計成這樣呢?
  這為開發者提供了方便,若希望將Map當做Cache來使用,並且限制大小,只需繼承LinkedHashMap並重寫removeEldestEntry(Entry<K,V> eldest)方法,像這樣:
private static final int MAX_ENTRIES = 100;
protected boolean removeEldestEntry(Map.Entry eldest) {
      return size() > MAX_ENTRIES;
 }
--------------------------------------------------------------------------------------------------------
  LinkedHashMap自定義了迭代器以及迭代規則,LinkedHashMap是通過內部的雙向連結串列來完成迭代的,遍歷時間與鍵值對總數成正比,而HashMap遍歷時間與容量成正比,所以通常情況下,LinkedHashMap遍歷效能是優於HashMap的,但是因為需要額外維護連結串列,所以折中來看,兩者效能相差無幾。
   //迭代器
    private abstract class LinkedHashIterator<T> implements Iterator<T> {
        //初始化下個節點引用
        Entry<K,V> nextEntry    = header.after;
        Entry<K,V> lastReturned = null;

        /**
         * 用於迭代期間快速失敗行為
         */
        int expectedModCount = modCount;
        
        //連結串列遍歷結束標誌,當下個節點為頭節點的時候
        public boolean hasNext() {
            return nextEntry != header;
        }

        //移除當前訪問的節點
        public void remove() {
            //lastReturned會在nextEntry方法中賦值
            if (lastReturned == null)
                throw new IllegalStateException();
            //快速失敗機制
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();

            LinkedHashMap.this.remove(lastReturned.key);
            lastReturned = null;
            //迭代器自身刪除節點,並不是其他執行緒修改Map結構,所以這裡要修改expectedModCount
            expectedModCount = modCount;
        }

        //返回連結串列下個節點的引用
        Entry<K,V> nextEntry() {
            //快速失敗機制
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            //連結串列為空情況
            if (nextEntry == header)
                throw new NoSuchElementException();
            
            //給lastReturned賦值,最近一個從迭代器返回的節點物件
            Entry<K,V> e = lastReturned = nextEntry;
            nextEntry = e.after;
            return e;
        }
    }
    //key迭代器
    private class KeyIterator extends LinkedHashIterator<K> {
        public K next() { return nextEntry().getKey(); }
    }
    //value迭代器
    private class ValueIterator extends LinkedHashIterator<V> {
        public V next() { return nextEntry().value; }
    }
    //key-value迭代器
    private class EntryIterator extends LinkedHashIterator<Map.Entry<K,V>> {
        public Map.Entry<K,V> next() { return nextEntry(); }
    }

    // 返回不同的迭代器物件
    Iterator<K> newKeyIterator()   { return new KeyIterator();   }
    Iterator<V> newValueIterator() { return new ValueIterator(); }
    Iterator<Map.Entry<K,V>> newEntryIterator() { return new EntryIterator(); }
總結:
    1.LinkedHashMap繼承自HashMap,具有HashMap的大部分特性, 比如支援null鍵和值,預設容量為16,裝載因子為0.75,非執行緒安全等等;
    2.LinkedHashMap通過設定accessOrder控制遍歷順序是按照插入順序還是按照訪問順序。當accessOrder為true時,可以利用其完成LRU快取的功能;
    3.LinkedHashMap內部維護了一個雙向迴圈連結串列,並且其迭代操作時通過連結串列完成的,而不是去遍歷hash表。
補充:
    重寫父類的containsValue(Object value)方法,直接通過header遍歷連結串列判斷是否有值和value相等,而不用查詢table陣列
    clear()方法先呼叫父類的方法clear()方法,之後將連結串列的header節點的before和after引用都指向header自身,即header節點就是一個雙向迴圈連結串列。這樣就無法訪問到原連結串列中剩餘的其他節點,他們都將被GC回收。
    /**
     * 覆蓋HashMap的transfer方法,效能優化,這裡遍歷方式不採用HashMap的雙重迴圈方式
     * 而是直接通過雙向連結串列遍歷Map中的所有key-value對映,
     */
    public boolean containsValue(Object value) {
        // Overridden to take advantage of faster iterator
        if (value==null) {
            for (Entry e = header.after; e != header; e = e.after)
                if (e.value==null)
                    return true;
        } else {
            for (Entry e = header.after; e != header; e = e.after)
                if (value.equals(e.value))
                    return true;
        }
        return false;
    }

    /**
     * 呼叫HashMap的clear方法,並將LinkedHashMap的頭結點前後引用指向自己
     */
    public void clear() {
        super.clear();
        header.before = header.after = header;
    }

------------------------------------------------------------------------------------------------

測試

    @Test
    public void testLinkedHashMap() {
        LinkedHashMap<String, Integer> mapPerson = new LinkedHashMap<String, Integer>(16, 0.75f, true );
        mapPerson.put("c", 3);
        mapPerson.put("a", 1);
        mapPerson.put("b", 2);
        mapPerson.get("c");
        mapPerson.get("b");  //跟這個順序有關

        for (Map.Entry<String, Integer> e : mapPerson.entrySet()) {
            System.out.println(e.getKey() + " " + e.getValue());
        }
        /* false:c 3 , a 1 , b 2 (按插入順序)
         *  true:a 1 , c 3 , b 2 (從近期訪問最少到近期訪問最多的順序)*/
    }


相關推薦

Java集合框架Map---HashMapLinkedHashMap原始碼分析

1、HashMap概述:    HashMap是基於雜湊表的Map介面的非同步實現。此實現提供所有可選的對映操作,並允許使用null值和null鍵。此類不保證對映的順序,特別是它不保證該順序恆久不變。 2、HashMap的資料結構 資料結構中有陣列和連結串列來實現對資料的

Java集合框架十三-------------HashMap的擴容與執行緒安全問題

HashMap擴容中,hash & oldCap的作用,觀察擴容前和擴容後下標的變化 原來的0101和10101在length=16的時候,通過hash&length-1的方法,計算出來都是0101;但是在擴容後即length=32時,hash&

Java集合框架三:HashMap原始碼解析 Java集合框架三:HashMap原始碼解析

Java集合框架之三:HashMap原始碼解析     版權宣告:本文為博主原創文章,轉載請註明出處,歡迎交流學習!       HashMap在我們的工作中應用的非常廣泛,在工作面試中也經常會被問到,對於這樣一個重要的集合

Java集合框架HashMap原始碼解析

1.首先看一下HashMap的繼承關係 java.lang.Object ↳ java.util.AbstractMap<K, V> ↳ java.util.HashMap<K, V> pub

JAVA集合框架List、Map、Set之間的選擇~小案例分析

案例分析 案例介紹:簡易撲克牌遊戲。 功能描述: 二:實現洗牌 三:實現發牌 四:得出輸贏 集合(list、set、map)的選擇 既然要比較,我們還是先從JAVA集合的祖先來介紹。 陣列 時間本沒有集合,但有人想要,所以有了集合

Java集合框架的接口類層次關系結構圖

fly tsv nsh ats cap war sdc groovy fmb %E7%94%A8groovy%E8%84%9A%E6%9C%AC%E8%BF%9B%E8%A1%8C%E6%AF%8F%E6%97%A5%E5%B7%A5%E4%BD%9C%E7%9A%84%E

集合框架Map學習

strong size keys 文章 接口 使用方法 tor entry ash Map接口的實現類有HashTable、HashMap、TreeMap等,文章學習整理了“ Map和HashMap的使用方法”。 /** * Map和HashMap的使用方法 */publi

java集合框架HashCode

封裝 app stringbu result ati des tor 平均值 http 參考http://how2j.cn/k/collection/collection-hashcode/371.html List查找的低效率 假設在List中存放著無重復名稱,沒有順序的

Java基礎知識(JAVA集合框架List與Set)

開發 如果 表數 特點 必須 加鎖 以及 stringbu 不可 List和Set概述數組必須存放同一種元素。StringBuffer必須轉換成字符串才能使用,如果想拿出單獨的一個元素幾乎不可能。數據有很多使用對象存,對象有很多,使用集合存。 集合容器因為內部

基於原始碼Java集合框架學習⑭ Map總結

Map概括 Map 是“鍵值對”對映的抽象介面。 AbstractMap 實現了Map中的絕大部分函式介面。它減少了“Map的實現類”的重複編碼。 SortedMap 有序的“鍵值對”對映介面。 NavigableMap 是繼承於SortedMap的,支援導航函式的介面。

java集合框架第二記LinkedHashSetLinkHashMap及類的學習方法

昨天CSDN維護,儲存的草稿又不見了,所以今天補上;不過又比較懶,就把兩個合在一塊寫了。 首先是類的學習方法,首先了解類的使用。LinkedHashSet和LinkedHashMap的使用和之前的差不多,都是使用.add()或者.put()等等,但是細節不一樣。在學習中瞭解

java:集合框架(ArrayList儲存字串自定義物件並遍歷泛型版)

A:案例演示     * ArrayList儲存字串並遍歷泛型版 import java.util.ArrayList; import java.util.Iterator; import com.

Java集合框架ArrayList原始碼分析

分析: java集合框架主要包括兩種型別的容器,集合(Collection),儲存元素集合,圖(Map),儲存鍵值對對映,而Collection介面又有三種子型別:List,Set,Queue,然後是一些抽象類,最後是一些實現類,常用ArrayList,Lin

Java集合框架Collection例項解析

0、集合引入 1)集合的由來? Java是面向物件程式語言,經常需要操作很多物件,必要時需儲存物件(對Java語言而言,儲存的通常是物件的引用,以達到複用或管理等目的),常見容器如陣列和StringBuffer(執行緒安全但效率較低,為了提高效率而

集合屬性Map屬性properties屬性

一:配置Map屬性 1.java.util.Map通過<map>標籤定義,<map>標籤裡可以使用多個<entry>作為子標籤,每個裡面都包含一個鍵和一個值 2.必須在key屬性定義鍵。 3.因為鍵和值的型別沒有限制,所以可以自由的為他們指定<v

Java集合框架 Collection介面

Collection介面是集合的根介面,它有兩個子介面分別是List介面和Set介面。 Collection介面的具體實類有ArrayList,LinkedList等對集合元素的增,刪,改,查。 使用前需要匯入相應的包import java.util.*; (1)

java集合框架List集合

list集合是工作中使用最頻繁的一種集合。它是一種有序的集合結構。可以用於儲存重複的元素。通過索引位置我們可以快速的定位元素進行操作。他的實現類主要有三個ArrayList,LinkedList,Vector。 ArrayList ArrayLis是使用最頻繁的一種集合。它是

Java集合框架Map與Set的有序與無序

Set本身不保證順序/* * HashSet是無序的; LinkedHashSet是按插入順序的; TreeSet是按升序的; * * HashMap是無序的;LinkedHashMap是按插

集合框架List ArrayListVector

array cto 默認 new 可變 操作 color 數據結構 拓展     ArrayList: ArrayList是List接口的實現類,其底層數據結構為數組,實現大小可變的數組。 ArrayList 是 線程不安全的 ,jdk1.2。 ArrayL

集合框架Map TreeMap

args add arraylist return style student 方式 dem bject TreeMap是Map接口的實現類,key以TreeSet方式存儲。 註意:TreeMap為二叉樹存儲,所以要在key中實現比較策略。 1 public cl