JDK 12 LinkedHashMap 源碼分析

LinkedHashMap 是什么参咙,能做什么,這里就不再展開講了。這篇博客 有相關介紹宠哄,并展示了 LinkedHashMap 的核心原理,但是我發(fā)現(xiàn)我的 jdk 里的源代碼和博主提供的源代碼示例不一致嗤攻,我的是 "12.0.1" 2019-04-16毛嫉,所以就寫了這篇文章,看看新版本的有哪些調整妇菱,以及為什么有這些調整承粤。

1. 類注釋

在類注釋中,總結一下大致有以下幾個要點:

  • 與 HashMap 不同恶耽,LinkedHashMap 維護了一個雙向鏈表來定義迭代順序
  • re-insert 一個已有的元素不會改變迭代順序
  • 迭代順序默認按插入順序,但是也可以初始化為按訪問順序颜启,這樣就很適合用來實現(xiàn) LRU cache
  • 在 LinkedHashMap 上迭代的時間復雜度是 O(size)偷俭,而在 HashMap 上迭代是 O(capacity). 其他操作基本上都是 O(1),但因為維護雙向鏈表的原因缰盏,性能上稍微遜于 HashMap
  • LinkedHashMap 的性能受 initial capacityload factor 的影響涌萤,這兩個參數(shù)是從 HashMap 繼承下來的,所以 HashMap 也是口猜;但是因為迭代策略的不同负溪,initial capacity 的值很大,不會直接影響迭代性能
  • 線程不安全济炎。因為繼承自 HashMap川抡,許多性質也一樣
  • add/delete 會影響到迭代順序;插入順序下须尚,改變 pair 的 value 不會影響崖堤;訪問順序下,get 操作也會影響到迭代順序耐床。這個根據(jù)定義很好理解
  • 迭代過程中如果被其他線程修改密幔,會以 fail-fast 的策略盡快拋出 ConcurrentModificationException,需要注意的是撩轰,這個也只是 best-effort 的行為胯甩,并不能保證在沖突的第一時間就拋出異常,所以捕獲異常后的 map 是不確定的
  • 應該是 JDK8 新增的并行部分堪嫂,暫時沒看 The spliterators returned by the spliterator method of the collections returned by all of this class's collection view methods are <a href="Spliterator.html#binding">late-binding</a></em>,<em>fail-fast</em>, and additionally report {@link Spliterator#ORDERED}.

2. 繼承結構

LinkedHashMap 繼承自 HashMap偎箫,重用了部分屬性,重寫了部分方法皆串;自己額外定義的主要包括一些內部類镜廉、構造函數(shù)等。


LinkedHashMap 的結構

3. 從一個 demo 開始

public class Test {

    public static void main(String[] args) {
        LinkedHashMap<String, String> map = new LinkedHashMap<>();
        map.put("111", "111");
        map.put("333", "333");
        map.put("222", "222");
        System.out.println(map);

        HashMap<String, String> hashMap = new HashMap<>();
        hashMap.put("111", "111");
        hashMap.put("333", "333");
        hashMap.put("222", "222");
        System.out.println(hashMap);
    }
}

// output
{111=111, 333=333, 222=222}
{111=111, 222=222, 333=333}

println 方法對 object 類型的參數(shù)愚战,會調用 object 的 toString() 方法娇唯;map 系列的這個方法是定義在 AbstractMap 里面的齐遵,拿到 entrySet 的 iterator,再通過 iterator 的 next 方法來迭代塔插。

// AbstractMap.java
public String toString() {
        Iterator<Entry<K,V>> i = entrySet().iterator();
        //...
        for (;;) {
            Entry<K,V> e = i.next();
        //...
        }
}

// AbstractMap.java
public abstract Set<Entry<K,V>> entrySet();

而 LinkedHashMap 和 HashMap 的 entrySet() 方法也不相同梗摇,

// LinkedHashMap.java
public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}

// HashMap.java
public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

所以主要是 LinkedEntrySet 和 EntrySet 導致的區(qū)別,下面列出了有區(qū)別的方法想许,可以發(fā)現(xiàn)實際上又是 Iterator 導致的區(qū)別

  final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new EntryIterator();
        }
        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 (Node<K,V> e : tab) {
                    for (; e != null; e = e.next)
                        action.accept(e);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
  }

  final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new LinkedEntryIterator();
        }
        public final Spliterator<Map.Entry<K,V>> spliterator() {
            return Spliterators.spliterator(this, Spliterator.SIZED |
                                            Spliterator.ORDERED |
                                            Spliterator.DISTINCT);
        }
        public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
            if (action == null)
                throw new NullPointerException();
            int mc = modCount;
            for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
                action.accept(e);
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }

兩者的 iterator 的 next()方法都是調用了

public final Map.Entry<K,V> next() { return nextNode(); }

首先注意伶授,nextNode() 返回的是初始化/上次計算 的 next 值,并計算出下一個值流纹。nextNode 在 LinkedHashMap 和 HashMap 中有者不同的實現(xiàn)糜烹,

先來看看 HashIterator
next 由 HashIterator 構造函數(shù)初始化,并在 nextNode 方法中更新漱凝。table 是 hash bucket疮蹦,一個數(shù)組。
首先是一個 fast-fail 地檢測是否被并發(fā) add/delete 了茸炒,(這個機制請參考其他博客愕乎,這里不再贅述),然后把指針在 table 上后移壁公,如果 next 不為空則直接返回感论;如果為空,則要跳過一個槽看下一個紊册,循環(huán)比肄;

所以,HashMap 的迭代復雜度是 O(capacity)囊陡,因為它需要檢查 table 上的每一個元素

  abstract class HashIterator {
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot
        
        HashIterator() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }
    }

再來看看 LinkedHashIterator
一樣的 fail-fast check薪前,但是神奇的地方出現(xiàn)了,next = e.after关斜,完事兒示括,完全不跟你多bb×⌒螅可以肯定垛膝,這個 after 指向的肯定是按 insert order/access order 的下一個元素。那么這個 after 又是哪里冒出來的呢丁稀?

  abstract class LinkedHashIterator {
        final LinkedHashMap.Entry<K,V> nextNode() {
            LinkedHashMap.Entry<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            current = e;
            next = e.after;
            return e;
        }
    }

所以吼拥,問題的核心還是回到了 LinkedHashMap.Entry 上。

在 Collections 框架里线衫,Entry 應該是接口凿可,用于定義鍵值對;實體類應該是 XXXNode 才對。對于這一點枯跑,源代碼中的注釋也給出了說明:HashMap now uses trees for some of its nodes, class LinkedHashMap.Entry is now treated as intermediary node class that can also be converted to tree form. The name of this class, LinkedHashMap.Entry, is confusing in several ways in its current context, but cannot be changed. but cannot be changed Otherwise, even though it is not exported outside this package, some existing source code is known to have relied on a symbol resolution corner case rule in calls to removeEldestEntry that suppressed compilation errors due to ambiguous usages. So, we keep the name to preserve unmodified compilability.
也就是說惨驶,一開始沒有考慮到規(guī)范性的問題,而 HashMap 又用了 LinkedHashMap.Entry 來實現(xiàn) TreeNode敛助;即使這個靜態(tài)內部類沒有暴露出去粗卜,但是有的程序,是通過名字來解析這個類的纳击,如果改了名字會導致編譯都過不了续扔,所以為了兼容就不改了。

HashMap 中定義了 Node焕数,LinkedHashMap.Entry 繼承自 Node纱昧。多了兩個屬性變量,before 和 after堡赔。根據(jù)名字我們可以猜到识脆,這是一個雙向鏈表的元素。源代碼如下加匈,但是初始化一個 Entry 的時候并沒有設置 before 和 after 信息存璃,那么雙向鏈表的維護必定是在 Map 的操作過程中仑荐。

  /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
  static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }   
  }

  /**
     * 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);
        }
  }

經(jīng)過上述分析雕拼,我們已經(jīng)清楚 LinkedHashMap 的大致結構和原理。下面我們來具體看看這個雙向鏈表是怎么維護的粘招。
回到我們一開始的程序

public class Test {

    public static void main(String[] args) {
        LinkedHashMap<String, String> map = new LinkedHashMap<>();
        map.put("111", "111");
        map.put("333", "333");
        map.put("222", "222");
        System.out.println(map);
    }
}

// output
{111=111, 333=333, 222=222}

檢查 LinkedHashMap 的構造函數(shù)啥寇,accessOrder 被設置為 false.

public LinkedHashMap() {
        super();
        accessOrder = false;
}

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

/**
 * 標識 LinkedHashMap 的迭代順序: {@code true}
 * for access-order, {@code false} for insertion-order.
 */
final boolean accessOrder;

再看 put 方法,直接就進入了 HashMap 里面洒扎。奇怪了對吧辑甜?沒有重寫 put 方法。那是在哪里設置的 before/after 呢?
PS:putVal 方法比較復雜袍冷,是個核心算法磷醋,可以研究下。

  public V put(K key, V value) {
      return putVal(hash(key), key, value, false, true);
  }

    /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = 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;
    }

HashMap 的 putVal 方法調用了 newNode()胡诗,afterNodeAccess() 等邓线。在 HashMap 的源代碼中可以看見如下注釋:

    /* ------------------------------------------------------------ */
    // LinkedHashMap support


    /*
     * The following package-protected methods are designed to be
     * overridden by LinkedHashMap, but not by any other subclass.
     * Nearly all other internal methods are also package-protected
     * but are declared final, so can be used by LinkedHashMap, view
     * classes, and HashSet.
     */

    // 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);
    }
    ...

而在 LinkedHashMap 中,這些方法都被重寫了


LinkedHashMap 重寫父類中的方法

以 newNode 為例煌恢,初始化一個 entry 后骇陈,調用 linkNodeLast 來維護 before/after 指針。到這里瑰抵,我們終于知道為什么 LinkedHashMap 有順序了你雌。LinkedHashMap 也需要在其他方法里補上對 before/after 的操作,這里不再逐一分析二汛。

  Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<>(hash, key, value, e);
        linkNodeLast(p);
        return p;
  }

// internal utilities

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

還有一件事婿崭,那就是如何通過 accessOrder 來區(qū)分 insert order/access order的拨拓。默認是 insert order,不需要做額外的操作逛球;而 access order千元,則需要在每次訪問 entry 后,調整 entry 的位置颤绕。HashMap 的設計者暴露出了一個 afterNodeAccess 回調幸海,以 LinkedHashMap#get(K) 方法為例,如果是 access order奥务,會執(zhí)行 afterNodeAccess(e)

    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;
    }

在 LinkedHashMap.afterNodeAccess 中物独,會判斷是否是 accessOrder,是的話把這個 entry 放到雙向鏈表的最后氯葬。至于為什么是最后挡篓,正常人應該是如 LRU cache 一樣放到最前,別問帚称,問就是最后官研。

  void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<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;
        }
  }

所以,LinkedHashMap 還提供了一個 removeEldestEntry 回調闯睹,默認為 false戏羽,子類可以重寫來實現(xiàn)當是否刪除最 eldest 的 entry。這個回調會在 put/putAll 方法時觸發(fā)楼吃,何為 eldest 呢始花?對于 insert order,eldest 就是 put/putAll 加入的(最后一個)元素孩锡;對于 access order酷宵,eldest 就是 head 指針指向的元素(對應前面的移到最后)。

  /**
     * @param    eldest The least recently inserted entry in the map, or if
     *           this is an access-ordered map, the least recently accessed
     *           entry.  This is the entry that will be removed it this
     *           method returns {@code true}.  If the map was empty prior
     *           to the {@code put} or {@code putAll} invocation resulting
     *           in this invocation, this will be the entry that was just
     *           inserted; in other words, if the map contains a single
     *           entry, the eldest entry is also the newest.
     * @return   {@code true} if the eldest entry should be removed
     *           from the map; {@code false} if it should be retained.
     */
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

這種回調設計還有好幾個躬窜,下面是一些定義在 HashMap 中的回調

    // Callbacks to allow LinkedHashMap post-actions
    void afterNodeAccess(Node<K,V> p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeRemoval(Node<K,V> p) { }

下面再看一下 access order 的驗證 demo浇垦,因為 "333" 被 "最近訪問" 了,所以他被移到了鏈表的最后荣挨。

    public static void main(String[] args) {
        LinkedHashMap<String, String> map = new LinkedHashMap<>(16, 0.75f, true);
        map.put("111", "111");
        map.put("333", "333");
        map.put("222", "222");
        System.out.println(map);

        map.get("333");
        System.out.println(map);
    }

// output
{111=111, 333=333, 222=222}
{111=111, 222=222, 333=333}

4. 如何實現(xiàn) LRU cache

這是 leetcode 上的一個實現(xiàn)男韧,思路很明顯了

class LRUCache extends LinkedHashMap<Integer, Integer> {
    private int capacity;
    public LRUCache(int capacity) {
        super(capacity, 0.8F, true);
        this.capacity = capacity;
    }
    
    public int get(int key) {
        return super.getOrDefault(key, -1);
    }
    
    public void put(int key, int value) {
        super.put(key, value);
    }
    
    @Override
    public boolean removeEldestEntry(Map.Entry<Integer, Integer> entry) {
        return size() > capacity;
    }
}
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市垦沉,隨后出現(xiàn)的幾起案子煌抒,更是在濱河造成了極大的恐慌,老刑警劉巖厕倍,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件寡壮,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機况既,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門这溅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人棒仍,你說我怎么就攤上這事悲靴。” “怎么了莫其?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵癞尚,是天一觀的道長。 經(jīng)常有香客問我乱陡,道長浇揩,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任憨颠,我火速辦了婚禮胳徽,結果婚禮上,老公的妹妹穿的比我還像新娘爽彤。我一直安慰自己养盗,他們只是感情好,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布适篙。 她就那樣靜靜地躺著往核,像睡著了一般。 火紅的嫁衣襯著肌膚如雪匙瘪。 梳的紋絲不亂的頭發(fā)上铆铆,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天蝶缀,我揣著相機與錄音丹喻,去河邊找鬼。 笑死翁都,一個胖子當著我的面吹牛碍论,可吹牛的內容都是我干的。 我是一名探鬼主播柄慰,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼鳍悠,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了坐搔?” 一聲冷哼從身側響起藏研,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎概行,沒想到半個月后蠢挡,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年业踏,在試婚紗的時候發(fā)現(xiàn)自己被綠了禽炬。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡勤家,死狀恐怖腹尖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情伐脖,我是刑警寧澤热幔,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站讼庇,受9級特大地震影響断凶,放射性物質發(fā)生泄漏。R本人自食惡果不足惜巫俺,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一认烁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧介汹,春花似錦却嗡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至叹卷,卻和暖如春撼港,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背骤竹。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工帝牡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蒙揣。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓靶溜,卻偏偏與公主長得像,于是被迫代替她去往敵國和親懒震。 傳聞我的和親對象是個殘疾皇子罩息,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內容