LinkedHashMap源碼解析

LinkedHashMap類介紹

LinkedHashMap類是構建在HashMap的基礎上的。HashMap是數組+鏈表+紅黑樹的複合數據結構。LinkedHashMap在HashMap的基礎上添加了head和tail指針。這兩個指針會將HashMap中的元素連接起來,組成一個鏈表。下面咱們經過圖片來看下LinkedHashMap的結構。 java

上面的圖片咱們看到了LinkedHashMap的結構。下面咱們將鏈表單獨拎出來,其結構以下:

LinkedHashMap類字段

/**
 * The head (eldest) of the doubly linked list.
 */
transient LinkedHashMapEntry<K,V> head;

/**
 * The tail (youngest) of the doubly linked list.
 */
transient LinkedHashMapEntry<K,V> tail;

/**
 * The iteration ordering method for this linked hash map: <tt>true</tt>
 * for access-order, <tt>false</tt> for insertion-order.
 *
 * @serial
 */
final boolean accessOrder;
複製代碼

上面的head和tail字段是表示鏈表的頭結點和尾節點的。accessOrder字段能夠定製LinkedHashMap的插入順序。這個後面咱們分析源碼的時候詳細介紹。node

LinkedHashMap的鏈表節點結構

上面介紹了LinkedHashMap不少特性,主要都是其鏈表的相關特性(其大部分特性仍是繼承HashMap的)。咱們來看下LinkedHashMap的節點結構:算法

/**
 * HashMap.Node subclass for normal LinkedHashMap entries.
 */
static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
    LinkedHashMapEntry<K,V> before, after;
    LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}
複製代碼

節點結構很清晰,只是添加了before和after節點,分別指向先後的兩個節點。數組

LinkedHashMap構造器

/**
 * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
 * with the specified initial capacity and load factor.
 *
 * @param  initialCapacity the initial capacity
 * @param  loadFactor      the load factor
 * @throws IllegalArgumentException if the initial capacity is negative
 *         or the load factor is nonpositive
 */
public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}

/**
 * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
 * with the specified initial capacity and a default load factor (0.75).
 *
 * @param  initialCapacity the initial capacity
 * @throws IllegalArgumentException if the initial capacity is negative
 */
public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    accessOrder = false;
}

/**
 * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
 * with the default initial capacity (16) and load factor (0.75).
 */
public LinkedHashMap() {
    super();
    accessOrder = false;
}

/**
 * Constructs an insertion-ordered <tt>LinkedHashMap</tt> instance with
 * the same mappings as the specified map.  The <tt>LinkedHashMap</tt>
 * instance is created with a default load factor (0.75) and an initial
 * capacity sufficient to hold the mappings in the specified map.
 *
 * @param  m the map whose mappings are to be placed in this map
 * @throws NullPointerException if the specified map is null
 */
public LinkedHashMap(Map<? extends K, ? extends V> m) {
    super();
    accessOrder = false;
    putMapEntries(m, false);
}

/**
 * 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;
}
複製代碼
  • 絕大多數的構造函數都會先調用HashMap的構造函數,而後將accessOrder設置爲false。LinkedHashMap默認的鏈表結構是插入順序的。
  • 咱們固然能夠設置accessOrder來修改LinkedHashMap的中鏈表的順序。

LinkedHashMap相關操做方法

絕大多數方法都是使用的HashMap的,有些方法是通過LinkedHashMap複寫的。下面咱們來一一介紹這些方法。緩存

LinkedHashMap的插入方法

咱們介紹HashMap中的putVal的時候確定看到以下代碼:bash

afterNodeInsertion(evict);
複製代碼

這個方法在HashMap中的實現是空的,LinkedHashMap複寫了這個方法,咱們來看源碼:數據結構

void afterNodeInsertion(boolean evict) { 
    // possibly remove eldest
    LinkedHashMapEntry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}
複製代碼

咱們看到若是evict參數是true而且有頭結點而且removeEldestEntry函數返回true的話就會刪除頭結點(這實際上是LRU算法)。 細心的同窗可能會問,上面不是說LinkedHashMap默認是實現是添加Node節點到鏈表尾部嗎?沒看見相關代碼啊,只是看到了afterNodeInsertion方法。咱們仔細的看源碼會發現,若是須要插入節點的話確定會調用newNode方法。咱們來看HashMap中和LinkedHashMap中newNode的實現:app

// Create a regular (non-tree) node
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
    return new Node<>(hash, key, value, next);
}

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMapEntry<K,V> p =
        new LinkedHashMapEntry<K,V>(hash, key, value, e);
    linkNodeLast(p);
    return p;
}
複製代碼

咱們看到在HashMap中newNode只是返回一個Node節點對象。在LinkedHashMap中會先構造一個LinkedHashMapEntry對象,而後調用linkNodeLast將其連接到鏈表的尾部。源碼以下:ide

// link at the end of list
private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
    LinkedHashMapEntry<K,V> last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}
複製代碼

get方法

/**
 * Returns the value to which the specified key is mapped,
 * or {@code null} if this map contains no mapping for the key.
 *
 * <p>More formally, if this map contains a mapping from a key
 * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
 * key.equals(k))}, then this method returns {@code v}; otherwise
 * it returns {@code null}.  (There can be at most one such mapping.)
 *
 * <p>A return value of {@code null} does not <i>necessarily</i>
 * indicate that the map contains no mapping for the key; it's also * possible that the map explicitly maps the key to {@code null}. * The {@link #containsKey containsKey} operation may be used to * distinguish these two cases.'
 */
public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}
複製代碼

主要仍是會調用getNode方法,其次會根據accessOrder參數決定是否調用afterNodeAccess方法。咱們來看下其相關邏輯:函數

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMapEntry<K,V> last;
    if (accessOrder && (last = tail) != e) {
        LinkedHashMapEntry<K,V> p =
            (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}
複製代碼

從源碼中咱們看到,若是accessOrder爲true就會將原來位置的節點e刪除,而後將其連接到鏈表的尾部。accessOrder參數設置爲true,LinkedHashMap就會使用符合LRU的特性,將最新訪問的節點移動到鏈表的尾部(注意:這個移動只是在鏈表中,元素實際的在HashMap的存儲仍是不變的)。

LinkedHashMap與LRU緩存

上面咱們或多或少已經介紹了使用LinkedHashMap來實現LRU的相關內容。這一節咱們詳細介紹下如何使用LInkedHashMap來定製本身的LRU算法。

  • 第一步就是將accessOrder設置爲true。accessOrder設置爲true,LinkedHashMap會將訪問操做都是LRU的方式來進行。
  • 須要覆蓋removeEldestEntry方法。LinkedHashMap默認實現以下:
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}
複製代碼

咱們能夠根據須要定製相關邏輯。下面咱們來看下一個簡單的例子

class LRUCache<T> extends LinkedHashMap {

    public LRUCache() {
    	super(16, 0.75f, true);
    }
    
    @Override
    protected boolean removeEldestEntry(java.util.Map.Entry eldest) {
    	return size()>3;
    }
}
複製代碼

在構造函數中咱們設置accessOrder爲true,讓LinkedHashMap符合LRU的相關特性。同時咱們複寫removeEldestEntry方法。當Map中的元素超過3個時候將最先添加的元素移除。咱們來些例子驗證下:

LRUCache<String> lruCache=new LRUCache<>();

lruCache.put("1","A");
lruCache.put("2","B");
lruCache.put("3","C");

System.out.println(lruCache);

//插入第4個元素(默認會移除第一個元素A)
lruCache.put("4","D");

System.out.println(lruCache);

//訪問元素,會將C移動到鏈表的尾部
lruCache.get("3");

System.out.println(lruCache);

//輸出以下:
{1=A, 2=B, 3=C}
{2=B, 3=C, 4=D}
{2=B, 4=D, 3=C}
複製代碼

總結

  • LinkedHashMap與HashMap相比,額外添加了一條鏈表將全部節點鏈接起來。這個鏈表是雙向的循環鏈表。
  • accessOrder參數默認是false。LinkedHashMap使用插入排序機制。保持默認的插入順序。若是將accessOrder設置爲true。那麼訪問LinkedHashMap中的元素將致使最新訪問的元素往鏈表尾部移動,符合LRU的訪問特性。
  • removeEldestEntry方法是刪除最老的元素,經過複寫該方法咱們能夠實現一個簡單的支持LRU的類。
  • LinkedHashMap的遍歷性能要更加好些,由於其能夠經過LinkedHashMap維護的鏈表進行遍歷。
相關文章
相關標籤/搜索