1. 程式人生 > >JDK1.8源碼逐字逐句帶你理解LinkedHashMap底層

JDK1.8源碼逐字逐句帶你理解LinkedHashMap底層

ret lean 一個 查詢 net out order 需要 als

數據存儲結構

我們已經知道HashMap是以散列表的形式存儲數據的,LinkedHashMap繼承了HashMap,所以LinkedHashMap其實也是散列表的結構,但是“linked”是它對HashMap功能的進一步增強,LinkedHashMap用雙向鏈表的結構,把所有存儲在HashMap中的數據連接起來。有人會說散列表不是已經有了鏈表的存儲結構了嘛,為什麽還要來個雙向鏈表?桶(桶的概念就是數組的一個存儲節點,比如說arr[0]是一個桶,arr[1]也是一個桶)中的鏈表和這個雙向鏈表是兩個概念,以下是我總結的區別:①桶中的鏈表是散列表結構的一部分;而雙向鏈表是LinkedHashMap的額外引入;②桶中的鏈表只做數據存儲,沒有存儲順序的概念;雙向鏈表的核心就是控制數據存儲順序(存儲順序是LinkedHashMap的核心);③桶中的鏈表產生是因為發生了hash碰撞,導致數據散落在一個桶中,用鏈表給予存儲,所以這個鏈表控制了一個桶;雙向鏈表是要串連所有的數據,也就是說有桶中的數據都是會被這個雙向鏈表管理。

所以,我修改了HashMap的圖片,大家參考下:
技術分享圖片
所以,簡單來說就是LinkedHashMap相比於HashMap來說就是多了這些紅色的雙向鏈表而已。

兩種演示

LinkedHashMap的核心就是存在存儲順序和可以實現LRU算法,所以下面我會用兩個demo先來證明這兩種情況:
①、放入到LinkedHashMap是有順序的,會按照你放入的順序存儲:

package com.brickworkers;
import java.util.LinkedHashMap;

/**
 * @author Brickworker
 * Date:2017年4月12日下午12:46:25 
 * 關於類LinkedHashMapTest.java的描述:jdk1.8逐字逐句帶你理解linkedHashMap
 * Copyright (c) 2017, brcikworker All Rights Reserved.
 */
public class LinkedHashMapTest {

    public static void main(String[] args) {
        LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer, Integer>();
        for (int i = 0; i < 10; i++) {//按順序放入1~9
            map.put(i, i);
        }
        System.out.println("原數據:"+map.toString());
        map.get(3);
        System.out.println("查詢存在的某一個:"+map.toString());
        map.put(4, 4);
        System.out.println("插入已存在的某一個:"+map.toString()); //直接調用已存在的toString方法,不然自己需要用叠代器實現
        map.put(10, 10);
        System.out.println("插入一個原本沒存在的:"+map.toString());
    }

    //輸出結果
//  原數據:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9}
//  查詢存在的某一個:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9}
//  插入已存在的某一個:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9}
//  插入一個原本沒存在的:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9, 10=10}

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

觀察以上代碼,其實它是符合先進先出的規則的,不管你怎麽查詢插入已存在的數據,不會對排序造成影響,如果有新插入的數據將會放在最尾部。

啟用LRU規則的LinkedHashMap,啟動這個規則需要在構造LinkedHashMap的時候,調用三個參數的構造器,這個構造器源碼如下:

    /**
     * Constructs an empty <tt>LinkedHashMap</tt> instance with the
     * specified initial capacity, load factor and ordering mode.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @param  accessOrder     the ordering mode - <tt>true</tt> for
     *         access-order, <tt>false</tt> for insertion-order
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;//是否開啟LRU規則
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

第三個參數accessOrder就是用於控制LRU規則的。
如下就是我寫的demo:

package com.brickworkers;

import java.util.LinkedHashMap;

/**
 * @author Brickworker
 * Date:2017年4月12日下午12:46:25 
 * 關於類LinkedHashMapTest.java的描述:jdk1.8逐字逐句帶你理解linkedHashMap
 * Copyright (c) 2017, brcikworker All Rights Reserved.
 */
public class LinkedHashMapTest {

    public static void main(String[] args) {
        LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer, Integer>(20, 0.75f, true);
        for (int i = 0; i < 10; i++) {//按順序放入1~9
            map.put(i, i);
        }
        System.out.println("原數據:"+map.toString());
        map.get(3);
        System.out.println("查詢存在的某一個:"+map.toString());
        map.put(4, 4);
        System.out.println("插入已存在的某一個:"+map.toString()); //直接調用已存在的toString方法,不然自己需要用叠代器實現
        map.put(10, 10);
        System.out.println("插入一個原本沒存在的:"+map.toString());
    }

    //輸出結果
//  原數據:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9}
//  查詢存在的某一個:{0=0, 1=1, 2=2, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9, 3=3} //被訪問(get)的3放到了最後面
//  插入已存在的某一個:{0=0, 1=1, 2=2, 5=5, 6=6, 7=7, 8=8, 9=9, 3=3, 4=4}//被訪問(put)的4放到了最後面
//  插入一個原本沒存在的:{0=0, 1=1, 2=2, 5=5, 6=6, 7=7, 8=8, 9=9, 3=3, 4=4, 10=10}//新增一個放到最後面

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

從上面可以看出,每當我get或者put一個已存在的數據,就會把這個數據放到雙向鏈表的尾部,put一個新的數據也會放到雙向鏈表的尾部。

逐字逐句底層源碼

接下來我們通過源碼深入學習LinkedHash,同時解答上述出現的有趣的事情。

分析LinkedHashMap的類名和繼承關系

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
{
  • 1
  • 2
  • 3
  • 4

從這裏我們可以看出LinkedHashMap是繼承了HashMap並實現了Map接口的,所以它和HashMap的關系肯定不一般。

分析LinkedHashMap的構造函數

//1
  public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }
//2
  public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }
//3
   public LinkedHashMap() {
        super();
        accessOrder = false;
    }

//4
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }
//5
  public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

它具有5個構造函數,可以設置容量和加載因子,且在默認情況下是不開啟LRU規則的。同時它還以用具有繼承K,V關系的map進行初始化。

分析雙向鏈表

    /**
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
    static class Entry<K,V> extends HashMap.Node<K,V> {
    //前後指針
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

    /**
     * The head (eldest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> head;//雙向鏈表頭節點(最老)

    /**
     * The tail (youngest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> tail;//雙向列表尾節點(最新)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

用一個靜態內部類Entry表示雙向鏈表,實現了HashMap中的Node內部類。before和after表示前後指針。我們在使用LinkedHashMap有序就是因此產生。

分析LRU規則實現,最近訪問放在雙向鏈表最後面

    void afterNodeAccess(Node<K,V> e) { // 把當前節點e放到雙向鏈表尾部
        LinkedHashMap.Entry<K,V> last;
        //accessOrder就是我們前面說的LRU控制,當它為true,同時e對象不是尾節點(如果訪問尾節點就不需要設置,該方法就是把節點放置到尾節點)
        if (accessOrder && (last = tail) != e) {
        //用a和b分別記錄該節點前面和後面的節點
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
             //釋放當前節點與後節點的關系 
            p.after = null;
            //如果當前節點的前節點是空,
            if (b == null)
            //那麽頭節點就設置為a
                head = a;
            else
            //如果b不為null,那麽b的後節點指向a
                b.after = a;
            //如果a節點不為空
            if (a != null)
                //a的後節點指向b
                a.before = b;
            else
                //如果a為空,那麽b就是尾節點
                last = b;
                //如果尾節點為空
            if (last == null)
            //那麽p為頭節點
                head = p;
            else {
            //否則就把p放到雙向鏈表最尾處
                p.before = last;
                last.after = p;
            }
            //設置尾節點為P
            tail = p;
            //LinkedHashMap對象操作次數+1
            ++modCount;
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

afterNodeAccess方法就是如何支持LRU規則的,如果在accessOrder為true的時候,節點調用這個函數,就會把該節點放置到最後面。put,get等都會調用這個函數來調整順序,我手畫了一張圖來表示這個函數幹了些什麽:
技術分享圖片

我們看看get方法中是否調用了此函數,以下是LinkedHashMap重寫了HashMap的get方法:

    public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)//如果啟用了LRU規則
            afterNodeAccess(e);//那麽把該節點移到雙向鏈表最後面
        return e.value;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

那麽有些小夥伴就問了,那麽put方法裏面調用了嘛?肯定調用了,但是你不一定找得到,因為LinkedHashMap壓根沒有重寫put方法,每次用LinkedHashMap的put方法的時候,其實你調用的都是HashMap的put方法。那為什麽它也會執行afterNodeAccess()方法呢,因為這個方法HashMap就是存在的,但是沒有實現,LinkedHashMap重寫了afterNodeAccess()這個方法。下面給出HashMap的put局部方法:

 if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);//把當前節點移動到雙向鏈表最尾處
                return oldValue;
            }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

其實這個方法在很多的調用都有,這裏就不一一解釋了。同時,LinkedHashMap對於紅黑樹的節點移動處理也有專門的方法,這裏就不再深入講解了,方式也是差不多。

分析一個LinkedHashMap自帶的移除最頭(最老)數據的方法

    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }
  • 1
  • 2
  • 3
  • 4

LinkedHashMap有一個自帶的移除最老數據的方法,但是這個方法永遠是返回false,但是如果我們要實現,可以在繼承的時候重寫這個方法,給定一個條件就可以控制存儲在LinkedHashMap中的最老數據何時刪除。具體的在我以前博文多種方式實現緩存機制中有寫過。觸發這個刪除機制,一般是在PUT一個數據進入的時候,但是LinkedHashMap並沒有重寫Put方法如何實現呢?在LinekdHashMap中,這個方法被包含在afterNodeInsertion()方法之中,而這個方法是重寫了HashMap的,但是HashMap中並沒有去實現它,所以在put的時候就會觸發刪除這個機制。

尾記

技術是不斷前進的,或許在JDK1.8的時候我寫的這些是有用的,但是下一個版本出來就不一定了。比如說前面幾個版本中,LinkedHashMap是一個循環的雙向鏈表,而且需要用init()方法進行初始化,但是後來都被移除了,以下是部分原本的linkedHashMap源碼:

 void init() {  
        header = new Entry<K,V>(-1, null, null, null);  
        header.before = header.after = header;  //循環的雙向鏈表
    }  
  • 1
  • 2
  • 3
  • 4

所以,在日常的學習中,尤其是技術,要與時俱進,在查詢某個技術點的時候,千萬要註意版本號,不一樣的版本之間可能是天差地別的。

JDK1.8源碼逐字逐句帶你理解LinkedHashMap底層