面試必備2:JDK1.8LinkedHashMap實現原理及源碼分析

首先附上我的幾篇其它文章鏈接感興趣的可以看看,如果文章有異議的地方歡迎指出,共同進步,順便點贊謝謝!!!
Android framework 源碼分析之Activity啓動流程(android 8.0)
Android studio編寫第一個NDK工程的過程詳解(附Demo下載地址)
面試必備1:HashMap(JDK1.8)原理以及源碼分析
Android事件分發機制原理及源碼分析
View事件的滑動衝突以及解決方案
Handler機制一篇文章深入分析Handler、Message、MessageQueue、Looper流程和源碼
Android三級緩存原理及用LruCache、DiskLruCache實現一個三級緩存的ImageLoader

概述

本文對LinkedHashMap的源碼分析是基於JDK1.8,因爲LinkedHashMap是在HashMap的基礎上進行的功能擴展,所以需要掌握HashMap的源碼和實現原理,如果不瞭解請先閱讀我的另一篇HashMap的實現原理和源碼分析

重點:本文如果有分析的不對的地方請大家留言指正!!!!!
再次強調一下:讀此文章之前需要先去了解HashMap的源碼請先閱讀我的另一篇HashMap的實現原理和源碼分析,以便理解。

LinkedHashMap的數據結構

想要知道 LinkedHashMap的實現原理,就必須先去了解它的數據結構(即存儲機制),和HashMap一樣LinkedHashMap的數據結構也是通過數組和鏈表組成的散列表,同樣線程也是不安全的允許null值null鍵,不同的是LinkedHashMap在散列表的基礎上內部維持的是一個雙向鏈表在每次增、刪、改、 查時增加或刪除或調整鏈表的節點順序。

  1. 在默認情況下LinkedHashMap遍歷時的順序是按照插入節點順序,我們可一再構造器中通過傳入accessOrder=true參數,使得其遍歷順序按照訪問的順序輸出。
  2. 因繼承自HashMap的一些特性LinkedHashMap都有,比如擴容的策略,哈希桶長度一定是2的N次方等等。
  3. 本質上LinkedHashMap是通過複寫父類HashMap的幾個抽象方法,去實現有序輸出

LinkedHashMap的鏈表節點LinkedHashMapEntry<K,V>:
LinkedHashMap與HashMap都是有數組和鏈表組成的散列表,不同的是LinkedHashMap的LinkedHashMapEntry<K,V>繼承HashMap的Node<K,V>,並在其基礎上進行擴展成一個雙向鏈表其源碼如下:

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);//其他的就是父類HashMap的Node
        }
    }

此外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;

增、改put(key,value)方法源碼

LinkedHashMap並沒有重寫HashMap的put、putVal()方法,只是重寫了putVal()中調用的以下三個方法

  1. 構建新節點時調用的newNode()方法,去構建LinkedHashMapEntry節點,而不是Node節點
  2. 節點被訪問後afterNodeAccess(e)抽象方法
  3. 節點被插入後afterNodeInsertion(evict)抽象方法

 public V put(K key, V value) {
 		//計算hash值,調用putVal方法,與父類相同,再此不做分析
        return putVal(hash(key), key, value, false, true);
    }

在這裏我將從這三個複寫的方法源碼進行分析,至於put()、putVal()方法的詳細分析請閱讀上文HashMap的實現原理和源碼分析中put方法源碼

1:重寫了newNode()方法源碼

不同的是重寫了在putVal()方法中調用的newNode方法,並且在創建新節點時將該節點鏈接在鏈表尾部 linkNodeLast(p)

 Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
       //創建的是LinkedHashMapEntry節點
        LinkedHashMapEntry<K,V> p =
            new LinkedHashMapEntry<K,V>(hash, key, value, e);
          //並且在創建新節點時將該節點鏈接在鏈表尾部
        linkNodeLast(p);
        return p;
    }

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

2:複寫了afterNodeAccess(Node e )

當節點被訪問後,即put的鍵已經存在時,調用afterNodeAccess(Node e)方法進行排序

    /**
     * 當put(key,value)中key存在的時候,即訪問某個存在的節點時
     * 如果assessOrder=true,將該節點移動到最後
     * @param e
     */
    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMapEntry<K,V> last;//記錄尾部節點的臨時變量
        if (accessOrder && (last = tail) != e) {//如果assessOrder&&該節點不在鏈表尾部則將其移動到尾部
            LinkedHashMapEntry<K,V> p =
                    (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;//p記錄當前訪問的節點  b和a分別記錄當前節點的前、後節點
            p.after = null;//將當前節點的after置null,因爲鏈表尾部節點沒有after節點
            //以下是斷鏈和重新連接鏈表的過程   
            if (b == null)
               //b==null表示,p的前置節點是null,即p以前是頭結點,所以更新現在的頭結點是p的後置節點a
                head = a;
            else
                //否則直接將當前節點的前後節點相連,移除當前節點
                b.after = a;
            if (a != null)
                 //a != null表示p的後置節點不是null,則更新後置節點a的前置節點爲b
                a.before = b;
            else//如果原本p的後置節點是null,則p就是尾節點。 此時 更新last的引用爲 p的前置節點b
                last = b;
            if (last == null)//原本尾節點是null  則,鏈表中就一個節點
                //記錄新的鏈表頭
                head = p;
            else {
                //否則 更新當前節點p的前置節點爲 原尾節點last, last的後置節點是p
                p.before = last;
                last.after = p;
            }
            //修改成員變量  爲節點爲當前節點
            tail = p;
            //修改modCount
            ++modCount;
        }
    }

3:複寫了afterNodeInsertion(Node e)

/**
     * 當插入新節點後,根據evict和判斷是否需要刪除最老插入的節點
     * @param evict   爲false時表示初始化時調用
     */
    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMapEntry<K,V> first;//記錄鏈表頭節點
        //是否初始化&&鏈表不爲null&&是否移除最老的節點,莫認返回false 則不刪除節點
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }

    /**
     *LinkedHashMap 默認返回false 則不刪除節點。 返回true 代表要刪除最早的節點。
     * 通常構建一個LruCache會在達到Cache的上限是返回true
     * @param eldest  移除最老的節點
     * @return
     */
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

注意: 這兩個方法void afterNodeInsertion(boolean evict)boolean removeEldestEntry(Map.Entry<K,V> eldest)是構建LruCache需要的回調,在LinkedHashMap裏可以忽略它們。

查:get(key)和getOrDefault( key, defaultValue)方法源碼

和父類HashMap相比複寫了get(key)和getOrDefault( key, defaultValue)兩個方法,其查找過程和父類的相同,只是在查找完成後根據構造器中的accessOrder=true時調用了afterNodeAccess(e)方法會將當前被訪問到的節點e,移動至內部的雙向鏈表的尾部

 public V get(Object key) {
        Node<K,V> e;
        //獲取Node節點過程和父類HashMap的相同,在這裏不做分析
        if ((e = getNode(hash(key), key)) == null)
            return null;
         //accessOrder,將當前訪問節點移動到雙向鏈表的尾部
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }


 /**
     * {@inheritDoc}
     */
    public V getOrDefault(Object key, V defaultValue) {
       Node<K,V> e;
        //獲取Node節點過程和父類HashMap的相同,在這裏不做分析
       if ((e = getNode(hash(key), key)) == null)
           return defaultValue;
           // //accessOrder,將當前訪問節點移動到雙向鏈表的尾部
       if (accessOrder)
           afterNodeAccess(e);
       return e.value;
   }

刪:remove()方法源碼

LinkedHashMap的remove()和HashMap的邏輯相同,故而沒有重寫removeNode()方法,在這裏不做過多分析,詳細過程請閱讀HashMap的實現原理和源碼分析中的remove()方法的源碼,只是重寫了removeNode()方法中刪除節點後調用的afterNodeRemoval()這個回調方法,其源碼如下:

/**
     * 在刪除節點e時,同步將e從雙向鏈表上刪除
     * @param e  被刪除的節點
     */
    void afterNodeRemoval(Node<K,V> e) { // unlink
        LinkedHashMapEntry<K,V> p =
                (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
        //待刪除節點 p 的前置後置節點都置空
        p.before = p.after = null;

        //如果前置節點是null,則現在的頭結點應該是後置節點a
        if (b == null)
            head = a;
        else
            //否則將前置節點b的後置節點指向a
            b.after = a;
        if (a == null)//同理如果後置節點時null ,則尾節點應是b
            tail = b;
        else
            //否則更新後置節點a的前置節點爲b
            a.before = b;
    }

擴展:複寫了containsValue(Object value)相比HashMap的實現,更爲高效。

 public boolean containsValue(Object value) {
 		//一次for循環遍歷鏈表,查找Value相同的
        for (LinkedHashMapEntry<K,V> e = head; e != null; e = e.after) {
            V v = e.value;
            if (v == value || (value != null && value.equals(v)))
                return true;
        }
        return false;
    }

而HashMap的containsValue方法,是由兩個for循環組成,查詢效率相對較低源碼如下:

public boolean containsValue(Object value) {
        Node<K,V>[] tab; V v;
        if ((tab = table) != null && size > 0) {
            for (int i = 0; i < tab.length; ++i) {//遍歷數組
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {//遍歷鏈表
                    if ((v = e.value) == value ||
                        (value != null && value.equals(v)))
                        return true;
                }
            }
        }
        return false;
    }

遍歷:entrySet()方法源碼

這裏需要和上文HashMap的entrySet()方法源碼對比分析,更容易理解
LinkedHashMap的entrySet()方法源碼:

 public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        //直接返回 LinkedEntrySet() 集合
        return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
    }

遍歷主要用的是LinkedEntrySet的Iterator<Map.Entry<K,V>> iterator()方法,LinkedEntrySet是LinkedHashMap 的內部類,繼承成AbstractSet<Map.Entry<K,V>>集合,其詳細源碼比較簡單不在這裏進行詳細分析

LinkedHashMap中內部類LinkedEntrySetiterator()方法

final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final int size()                 { return size; }
        public final void clear()               { LinkedHashMap.this.clear(); }
        public final Iterator<Map.Entry<K,V>> iterator() {
        	//放回LinkedEntryIterator迭代器
            return new LinkedEntryIterator();
        }
        ....省略部分源碼...
    }

通過上文HashMap的extrySet方法源碼和以上源碼分析可以知道,當我們HashMap或LinkedHashMap調用entrySet()的Itrrator()方法返回的Iterator不同,分別是new EntryIterator();new LinkedEntryIterator(),通過返回的Iterator對象進行集合的遍歷過程,接下來我將對其其源碼入手分析集合的遍歷過程

LinkedHashMap的迭代器LinkedEntryIterator源碼

final class LinkedEntryIterator extends LinkedHashIterator
        implements Iterator<Map.Entry<K,V>> {
        //迭代器的next方法就是返回通過調用`nextNode()`返回下一個節點
        public final Map.Entry<K,V> next() { return nextNode(); }
    }

nextNode()方法是父類LinkedHashIterator中的方法,LinkedHashMap的本質LinkedHashIterator是實現集合遍歷,其源碼分析如下

abstract class LinkedHashIterator {
        LinkedHashMapEntry<K,V> next;//下一個節點
        LinkedHashMapEntry<K,V> current;//當前操作的節點
        int expectedModCount;
		 /**
            * 構造器做初始化動作
           * 需要注意的是:expectedModCount的作用:
           * 和HashMap一樣,LinkedHashMap不是線程安全的,所以在迭代的時候,會將modCount賦值到迭代器的expectedModCount屬性中,
           * 然後進行迭代,如果在迭代的過程中HashMap被其他線程修改了,modCount的數值就會發生變化,
          * 這個時候expectedModCount和ModCount不相等,迭代器就會拋出 ConcurrentModificationException()異常
     */
        LinkedHashIterator() {
        	//初始化的時候next爲雙向鏈表的表頭
            next = head;
           //遍歷前,先記錄modCount值
            expectedModCount = modCount;
            //當前節點初始化爲null
            current = null;
        }
        public final boolean hasNext() {
        	//判斷是否有下一個節點,即判斷next是否爲null
            return next != null;
        }
		
		/**
          * nextNode()的方法,就是Iterator中next方法中調用的,
         * 其遍歷LinkedHashMap過程就是,就是從內部維護的雙向鏈表的表頭開始循環輸出。
        */
        final LinkedHashMapEntry<K,V> nextNode() {
        	//e用於記錄返回的節點
            LinkedHashMapEntry<K,V> e = next;
            //線程安全判斷處理
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
             //current  =next
            current = e;
            //next指向next的下一個節點,遍歷鏈表
            next = e.after;
            return e;
        }
		 /**
           * Iterator刪除方法 本質上還是調用了HashMap的removeNode方法
          * 只是在調用之前,通過modCount != expectedModCount時拋出併發修改異常,處理線程不安全問題,
          * 如果相等則調用HashMap的removeNode方法移除節點
          * 
        */
        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            //調用HashMap的,removeNode方法
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }

HashMap的遍歷本質上是通過Iterator的next()方法實現,而next()就是調用nextNode()方法,從nextNode的源碼可以看出:迭代LinkedHashMap,就是從內部維護的雙鏈表的表頭開始循環輸出。而雙鏈表節點的順序在LinkedHashMap的增、刪、改、查時都會更新。以滿足按照插入順序輸出,還是訪問順序輸出。

總結

本文是在上文HashMap的實現原理和源碼分析基礎上做的分析

  1. LinkedHashMap的數據結構就是在HashMap的散列表的基礎上維持了一個雙向鏈表,在每次增、刪、改、 查時增加或刪除或調整鏈表的節點順序。
  2. LinkedHashMap就是複寫了HashMap提供的幾個抽象方法,在每次插入數據,或者訪問、修改數據時,會增加節點、或調整鏈表的節點順序以改變它迭代遍歷時的順序
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章