1. 程式人生 > 實用技巧 >LinkedHashMap原始碼解析-Java8

LinkedHashMap原始碼解析-Java8

目錄

一.介紹

  1.1 HashMap無法保證順序

  1.2 如何保證HashMap的順序

  1.3 使用LinkedHashMap

  1.4 LinkedHashMap的順序分類

  1.5 LinkedHashMap使用示例

二.LinkedHashMap原始碼分析

  2.1 LinkedHashMap原理概覽

  2.2 連結串列的節點型別

  2.3 新增的屬性

  2.4構造方法

  2.5 LinkedHashMap的put

  2.6some post-actions

    2.6.1afterNodeAccess

    2.6.2afterNodeInsertion

    2.6.3 afterNodeRemoval

    2.6.3internalNodeWrit

  2.7LinkedHashMap的get

三. 總結

  

  

一.介紹

1.1 HashMap無法保證順序

  對於HashMap而言,將資料放入其中後,如果我們需要遍歷map中的所有元素,可以這麼做:

    1.通過map.entrySet()來獲取包含所有資料的set,對該set進行遍歷;

    2.通過map.keySet()獲取所有的key,然後遍歷key,呼叫get方法獲取value;

  上面這兩種方式都是可以進行map元素的遍歷,但是同時也存在一個問題:資料的順序無法保證,也就是說,在打印出所有元素前,元素的順序是未知的。

1.2 如何保證HashMap的順序

  因為HashMap不能保證順序,就需要藉助其他的資料結構,比如可以額外使用一個List(比如ArrayList),每次加入map後,同時再加入ArrayList,如果需要順序遍歷資料,則直接遍歷ArrayList即可,就不用遍歷map了。

  但是這樣的話,開銷就會相對較高,首先需要建立ArrayList,還需要每次增刪元素後,都要對ArrayList進行操作(移動元素),時間複雜度O(n),空間複雜度O(n)。

  

1.3 使用LinkedHashMap

  上面使用ArrayList來保證資料的順序性,當元素髮生變化時,就需要移動ArrayList中的元素,時間複雜度O(n),空間複雜度為O(n),那麼如果可以降低時間複雜度和空間複雜度就好了。

  於是,就會聯想到連結串列,不考慮查詢元素的開銷,增刪元素的時間複雜度為O(1);但是還是需要O(n)的空間複雜度,因為需要儲存節點值,如果能把空間複雜度降下來就好了,此時就可以瞭解一下LinkedHashMap。

  LinkedHashMap,其實從名字上就可以看出他是功能,Linked+HashMap,說到Linked,自然就會聯絡到LinkedList,所以,LinkedhashMap可以簡單的理解為LinkedList+HashMap。

  需要注意的是,LinkedHashMap並沒有完全建立一個新的節點型別,而是在HashMap的Node內部類上進行擴充,增加前後指標,這樣也是可以節省很多空間的。

1.4 LinkedHashMap的順序分類

  LinkedHashMap可以保證HashMap元素的順序,這個順序,有兩種:

  1.遍歷元素的順序與插入元素的順序一致,也就是說,依次插入A->B->C,那麼遍歷時的順序就是A->B->C;

  2.初始時,遍歷元素的順序與插入的順序一致,但是當元素被範文,也就是說,插入A->B->C後,遍歷的順序是A->B->C;如果中間訪問訪問或者修改過B,那麼順序就會變為A->C->B,

  在LinkedHashMap中,有一個accessOrder屬性來設定是否按照訪問順序來遍歷,預設為false,也就是上面預設使用第一種方式。

1.5 LinkedHashMap使用示例

package cn.ganlixin.util;
import org.junit.Test;
import java.util.LinkedHashMap;
import java.util.Map;

public class TestLinkedHashMap {

    /**
     * 遍歷順序和插入順序相同,期間訪問或者修改元素,順序不會發生改變
     */
    @Test
    public void testNormal() {
        Map<String, String> map = new LinkedHashMap<>();

        map.put("one", "1111");
        map.put("two", "2222");
        map.put("three", "3333");

        for (Map.Entry entry : map.entrySet()) {
            System.out.println(entry.getKey() + "=>" + entry.getValue());
        }
        /** 輸出如下:
         one=>1111
         two=>2222
         three=>3333
         */

        map.put("two", "2");
        for (Map.Entry entry : map.entrySet()) {
            System.out.println(entry.getKey() + "=>" + entry.getValue());
        }
        /** 輸出如下:
         one=>1111
         two=>2222
         three=>3333
         */
    }

    /**
     * 遍歷順序為訪問(修改)順序,元素被訪問或者被修改後,會移到最後
     */
    @Test
    public void testAccessOrder() {
        // public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
        Map<String, String> map = new LinkedHashMap<>(16, 0.75f, true);
        // 建立map時,設定遍歷順序按照訪問順序

        map.put("one", "1111");
        map.put("two", "2222");
        map.put("three", "3333");

        for (Map.Entry entry : map.entrySet()) {
            System.out.println(entry.getKey() + "=>" + entry.getValue());
        }
        /**
         one=>1111
         two=>2222
         three=>3333
         */

        map.get("one"); // 某個元素訪問後,會將該元素移動到末尾
        for (Map.Entry entry : map.entrySet()) {
            System.out.println(entry.getKey() + "=>" + entry.getValue());
        }
        /**
         two=>2222
         three=>3333
         one=>1111
         */

        map.put("two", "2"); // 修改元素後,也會將元素移動到最後
        for (Map.Entry entry : map.entrySet()) {
            System.out.println(entry.getKey() + "=>" + entry.getValue());
        }
        /**
         three=>3333
         one=>1111
         two=>2
         */

        map.replace("three", "3"); // 修改元素後,也會將元素移動到最後
        for (Map.Entry entry : map.entrySet()) {
            System.out.println(entry.getKey() + "=>" + entry.getValue());
        }
        /**
         three=>3333
         one=>1111
         two=>2
         */
    }
}

  

二.LinkedHashMap原始碼解析

2.1 LinkedHashMap原理概覽

  在閱讀LinkedHashMap前,要先了解HashMap的原理,可以參考:HashMap原始碼解析-Java8,方便後面對LinkedHashMap原始碼的理解。

  LinkedHashMap繼承自HashMap,也就是說HashMap的功能,LinkedHashMap都支援。

public class LinkedHashMap<K, V> extends HashMap<K, V> implements Map<K, V> 

  LinkedHashMap對HashMap的部分API進行了重寫,以此來保證map中元素遍歷時的順序。通過第一部分的一些介紹,其實我們大概才出LinkedHashMap是如何保證順序的:

  1.對於遍歷順序與插入順序相同的情況,只需要在將元素put到雙鏈表後,維護節點的指標,鏈入上一次put的節點後面(成為尾結點);

  2.對於遍歷順序與訪問順序相同一致的情況,只需要在get、put、replace..操作之後,將節點移動到末尾即可。

2.2 雙鏈表的節點

  LinkedHashMap中雙鏈表的節點型別,是直接在HashMap的節點型別上進行擴充的,增加了before和after指標;

/**
 * 連結串列的節點型別Entry,繼承自HashMap的Node內部類
 */
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);
    }
}

  

2.3 新增屬性

  LinkedHashMap繼承自HashMap,也繼承了HashMap中的所有屬性,比如預設的初始容量、預設的負載因子、預設轉換為紅黑樹和連結串列的閾值...

  除此之前,LinkedHashMap增加了3個額外的屬性,其中兩個屬性用於實現雙鏈表的頭尾節點,另外一個屬性用於控制順序的型別:

/**
 * 指向雙鏈表的頭結點(如果map為空=>雙鏈表為空=>head為空)
 */
transient LinkedHashMap.Entry<K, V> head;

/**
 * 指向雙鏈表的尾結點(如果map為空=>雙鏈表為空=>tail為空)
 */
transient LinkedHashMap.Entry<K, V> tail;

/**
 * 設定LinkedHashMap的順序型別
 * false:表示遍歷元素的順序與插入順序相同
 * true:表示遍歷元素的順序按照訪問的順序排列,當一個數據被訪問(修改)後,該資料就會移動到最後
 */
final boolean accessOrder;

  

2.4構造方法

  LinkedHashMap的構造方法,其實主要有兩個點:1.設定HashMap的初始容量和負載因子;2.設定順序型別(accessOrder)。  

/**
 * 建立LinkedHashMap,使用HashMap預設的初始容量16,預設的負載因子0.75
 */
public LinkedHashMap() {
    super();
    accessOrder = false;
}

/**
 * 建立LinkedHashMap,指定HashMap的初始容量,使用預設的負載因子0.75
 */
public LinkedHashMap(int initialCapacity) {
    // 指定HashMap的初始容量
    super(initialCapacity);
    accessOrder = false;
}

/**
 * 建立LinkedHashMap,指定HashMap的初始容量和負載因子
 */
public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}

/**
 * 建立LinkedHashMap,指定HashMap初始容量和負載因子,以及是否順序獲取
 */
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

/**
 * 建立LinkedHashMap,使用HashMap預設的初始容量(16)和負載因子(0.75)
 * 並且將傳入的map放入到LinkedHashMap中
 */
public LinkedHashMap(Map<? extends K, ? extends V> m) {
    super();
    accessOrder = false;
    putMapEntries(m, false);
}

  

2.5 LinkedHashMap的put

  LinkedHashMap並沒有覆蓋HashMap的put方法,而是直接沿用HashMap的put方法。

  那麼LinkedHashMap是如何保證順序的呢?

  是這樣的,HashMap在put的時候:

  1.如果是新增節點,那麼就會建立一個節點,然後放入到map中;

  2.如果put操作時修改操作(也就是put的key已經存在),那麼不需要建立節點,只需要修改已有節點的value即可;

  對於第一種情況,建立節點,是HashMap和LinkedHashMap都有的,LinkedHashMap重寫了HashMap建立新節點的方法(newNode和newTreeNode兩個方法),在這個時候將新建立的節點加入連結串列的末尾:

/**
 * HashMap的newNode,建立一個Node節點
 */
Node<K, V> newNode(int hash, K key, V value, Node<K, V> next) {
    return new Node<>(hash, key, value, next);
}

/**
 * HashMap中的newTreeNode,建立一個紅黑樹的節點
 */
TreeNode<K, V> newTreeNode(int hash, K key, V value, Node<K, V> next) {
    return new TreeNode<>(hash, key, value, next);
}

/**
 * LinkedHashMap重寫HashMap的newNode方法,
 * 建立一個雙鏈表節點,同時將新節點作為雙鏈表的尾結點
 */
@Override
Node<K, V> newNode(int hash, K key, V value, Node<K, V> e) {
    // 建立雙鏈表節點
    LinkedHashMap.Entry<K, V> p = new LinkedHashMap.Entry<K, V>(hash, key, value, e);

    // 將新建立的節點加入連結串列尾部
    linkNodeLast(p);
    return p;
}

/**
 * LinkedHashMap重寫HashMap的newTreeNode方法
 * 建立一個紅黑樹的節點,並將節點加入到雙鏈表的最後
 */
@Override
TreeNode<K, V> newTreeNode(int hash, K key, V value, Node<K, V> next) {
    TreeNode<K, V> p = new TreeNode<K, V>(hash, key, value, next);

    // 將新建立的節點加入連結串列尾部
    linkNodeLast(p);
    return p;
}

/**
 * 將節點加入連結串列中(掛在最後位置)
 */
private void linkNodeLast(LinkedHashMap.Entry<K, V> p) {
    // tail指向的尾結點
    LinkedHashMap.Entry<K, V> last = tail;

    tail = p;
    if (last == null) {
        // last==null,證明插入節點前連結串列為空,此時head也需要指向p
        head = p;

    } else {
        // 插入節點前,連結串列不為空,維護指標關係,將p插入到最後
        p.before = last;
        last.after = p;
    }
}

  對於put方法,進行了替換操作,就需要用到後面說的一些post-actions(後置處理)來完成順序的保證。

2.6 some post-actions

  由於LinkedHashMap是繼承自HashMap,並且大部分的程式碼都沒有做更改,也就是直接沿用HashMap的介面。那麼LinkedHashMap是如何保證順序的呢?特別是前面說的保證插入的順序,保證訪問的順序??

  其實HashMap的一些API中,比如put方法,其中就包含了一些callback,當插入元素或者查詢到元素後,就呼叫某個方法;這些callback方法在HashMap中沒有進行任何操作(方法體為空),留給子類LinkedHashMap進行實現的,如下面所示:

// Callbacks to allow LinkedHashMap post-actions

/**
 * 當節點被訪問後呼叫,比如
 *
 * @param p 被訪問的節點
 */
void afterNodeAccess(Node<K, V> p) {
}

/**
 * 當元素新增到map後,執行的操作
 * @param evict 
 */
void afterNodeInsertion(boolean evict) {
}

/**
 * 當元素被移除後,執行的操作
 * @param p 被移除的節點
 */
void afterNodeRemoval(Node<K, V> p) {
}

  

2.6.1 afterNodeAccess

  afterNodeAccess方法,是在元素節點被訪問之後被呼叫,主要是以下幾種(一部分):

  1.get操作

  2.put操作,如果是替換,那麼就會先查詢是否存在已有節點,若發現已經存在該節點(則該節點被訪問),就會回撥afterNodeAccess方法;

  3.遍歷操作

  在LinkedHashMap中afterNodeAccess,主要執行的就是將元素移動到連結串列的最後,前提是accessOrder為true(在訪問元素後,就將元素移動到連結串列移動到最後)。

/**
 * accessOrder為true時,表示map中元素保持和訪問的順序相同
 * 所以需要將訪問的節點移動到雙鏈表的末尾
 *
 * @param e 包含元素最新值的Node節點
 */
void afterNodeAccess(Node<K, V> e) { // move node to last
    LinkedHashMap.Entry<K, V> last;

    // accessOrder為true,表示map中資料的順序按照訪問順序排序
    // 如果e不是最後一個元素才進行操作(因為
    if (accessOrder && (last = tail) != e) {

        // p指向e,b指向e的前繼節點,a指向e的後繼節點
        LinkedHashMap.Entry<K, V> p = (LinkedHashMap.Entry<K, V>) e, b = p.before, a = p.after;

        // 將p的後繼設為null(因為要將其設為尾結點,尾結點的after為null)
        p.after = null;

        if (b == null) {
            // e的前繼節點為null,證明e為頭結點,則將e的後繼節點設定為新頭結點
            head = a;
        } else {
            // e不是頭結點,則將e的後繼節點設定為前繼節點的後繼節點
            b.after = a;
        }

        if (a != null) {
            // e節點不是尾結點,那麼就將e的前繼節點設定為後繼節點的前繼節點
            a.before = b;
        } else {
            // e的後繼節點為null,證明e為尾結點,則將e的前繼節點設定為尾結點
            last = b;
        }

        if (last == null) {
            // 如果修改指標關係(刪除e後),last為null,也就是尾結點為null,證明連結串列為空了
            // 此時將p節點(也就是e節點)作為頭結點
            head = p;
        } else {

            // 連結串列不為null,則將e節點掛到最後
            p.before = last;
            last.after = p;
        }

        // tail指標指向e節點
        tail = p;

        // 修改次數加1
        ++modCount;
    }
}

2.6.2 afterNodeInsertion

  afterNodeInsertion方法,是在HashMap的put和putAll方法呼叫後執行的,主要做的就是刪除連結串列的頭結點。

  在LinkedHashMap中,預設是不會每次刪除頭結點的,如果需要加入節點後進行刪除頭結點,則需要另外建立子類進行實現邏輯。

  上面提到一個eldestEntry,也就是最老的Entry,其實也就是連結串列的頭結點(accessOrder=false->最早插入,accessOrder=true->最久未訪問)。

2.6.3 afterNodeRemoval

  當呼叫LinkedHashMap的remove介面,其實也就是呼叫HashMap的remove介面(LinkedHashMap沒有重寫該介面)。

  在HashMap中移除元素後,會回撥afterNodeRemoval方法,該方法在HashMap中並做任何操作,而是留給子類LinkedHashMap進行定義對應的操作,LinkedHashMap做的就是維護連結串列指標,將刪除的元素從連結串列中刪除。

/**
 * LinkedHashMap刪除元素後,維護連結串列間的指標關係,將節點從雙鏈表中刪除
 */
@Override
void afterNodeRemoval(Node<K, V> e) { // unlink
    LinkedHashMap.Entry<K, V> p = (LinkedHashMap.Entry<K, V>) e, b = p.before, a = p.after;
    p.before = p.after = null;

    if (b == null) {
        // b為e的前繼節點,如果前繼節點為null,證明e為頭結點,則將第二個節點(e的後繼)作為新的頭結點
        head = a;
    } else {
        // e不是頭結點,維護前繼和後繼的關係
        b.after = a;
    }

    if (a == null) {
        // 後繼節點為空,表示e為尾結點,此時將e的前繼節點b作為尾結點
        tail = b;
    } else {
        // e不是尾結點,則前繼和後繼的關係
        a.before = b;
    }
}

  

2.6.4 internalWriteEntries

  當序列化map的時候(使用ObjectOutputStream),會呼叫HashMap中的writeObject方法

/**
 * 呼叫ObjectOutputStream來序列化map時,會呼叫writeObject方法
 */
private void writeObject(java.io.ObjectOutputStream s) throws IOException {
    int buckets = capacity();
    
    // 將一些屬性寫入到流中
    s.defaultWriteObject();
    s.writeInt(buckets);
    s.writeInt(size);

    // 將資料寫入
    internalWriteEntries(s);
}

/**
 * HashMap中的實現,將map中的資料順序寫入
 */
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
    Node<K, V>[] tab;
    // 如果map不為空,則遍歷陣列的每個位置進行操作
    if (size > 0 && (tab = table) != null) {
        for (int i = 0; i < tab.length; ++i) {

            // 每個位置可能是連結串列或者紅黑樹結構,則進行遍歷時寫入
            for (Node<K, V> e = tab[i]; e != null; e = e.next) {
                s.writeObject(e.key);
                s.writeObject(e.value);
            }
        }
    }
}

  internalWriteEntries方法,就是在輸出的時候決定資料的順序,可以看到上面是HashMap的internalWriteEntries方法,是依次遍歷陣列的每個位置,然後遍歷每個位置上的連結串列或者紅黑樹,這個時候順序並沒有什麼規律。

  而LinkedHashMap,只是重寫了internalWriteEntries方法,在其中對順序進行調整:

/**
 * LinkedHashMap中的實現
 */
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
    // 遍歷雙鏈表,從頭到尾進行遍歷,保證了順序
    for (LinkedHashMap.Entry<K, V> e = head; e != null; e = e.after) {
        s.writeObject(e.key);
        s.writeObject(e.value);
    }
}

2.7 LinkedHashMap.get方法

/**
 * 獲取key對應的值
 */
public V get(Object key) {
    Node<K, V> e;

    // 呼叫HashMap中的getNode
    if ((e = getNode(hash(key), key)) == null) {
        return null;
    }

    // 如果設定的順序是按照訪問順序,那麼就需要在訪問節點後,將節點移到末尾
    if (accessOrder) {
        afterNodeAccess(e);
    }

    return e.value;
}

  

三.總結

  其實對於LinkedHashMap,沒有太多的需要闡述的,更多的應該是看HashMap,因為維護節點順序的操作都是在HashMap中進行回撥的。

  簡單理解LinkedHashMap,也就是利用雙鏈表資料結構,在HashMap的Node節點型別上增加前後指標,每次訪問或者修改節點後,會回撥相關的callback,進行節點順序的維護。節點的順序可以分為插入順序和訪問順序,通過accessOrder進行控制。

四.常見的面試題

  一般是問LinkedHashMap的原理,比如:

  1.節點型別

  2.底層是雙鏈表還是單鏈表

  3.節點的順序(accessOrder)

  4.幾個擴充套件點(afterNodeAccess、afterNodeInsertion、afterNodeRemoval)

  而關於節點的順序,如果時間長了,可能會忘記,這裡簡單記一下:

  對於accessOrder為false,也就是預設情況下,遍歷時元素的順序只與插入的順序有關;如果某個key對應的資料已經存在,再次put、replace該key後,元素的順序不會發生改變。

  對於accessOrder為true,將元素加入map後:

    1.在沒有訪問的情況下,遍歷map元素,順序與插入順序一致;

    2.訪問元素(access),包括get、put、replace操作,無論是替換還是新增,元素都將移動到最後!!

  原文地址:https://www.cnblogs.com/-beyond/p/13412164.html