LinkedHashMap源碼解析(二)

  • 前言

       前文中已經解析了LinkedHashMap的插入操作,LinkedHashMap源碼解析一,接下來我們接着看它剩餘的操作。

  • 取數據操作

       首先我們看linkedHashMap的get的2個方法

 public V get(Object key) {
        Node<K,V> e;
        //第一步是直接使用Hashmap中的函數getNode方法,獲取value,如果爲null,返回null
        if ((e = getNode(hash(key), key)) == null)
            return null;

        //如果構造函數accessOrder爲true,那麼就把方法的這個函數放置到雙向鏈表的末尾。
        //這就是最新使用的元素。
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

 //與get相比多了一個返回默認值的功能
 public V getOrDefault(Object key, V defaultValue) {
       Node<K,V> e;
       if ((e = getNode(hash(key), key)) == null)
           return defaultValue;
       if (accessOrder)
           afterNodeAccess(e);
       return e.value;
   }

      接下來看HashMap的getNode的方法。

    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            
            //如果此key對應的hash桶不爲空執行以下

            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
            //如果需要查詢的是此哈希桶的第一個元素,直接返回
                return first;
            if ((e = first.next) != null) {
                //如果是紅黑樹的結果,就需要按照紅黑樹的方式查找
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);

                do {
                    //遍歷整個鏈表,找到key對應的value並且返回
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

       接下來我們看看afterNodeAccess這個函數,來了解linkedHashMap的操作方式

//首先要明確這個函數的目的是將訪問的這個元素移動到雙向鏈表的尾端,並且要將
//他的後面的元素向前移動

void afterNodeAccess(Node<K,V> e) {
        //last爲原鏈表的尾節點
        LinkedHashMapEntry<K,V> last;

        //如果accessOrder爲true,並且尾端元素不是需要訪問的元素
        if (accessOrder && (last = tail) != e) {

            //將節點e強制轉換成linkedHashMapEntry,b爲這個節點的前一個節點,a爲它的後一個節點

            LinkedHashMapEntry<K,V> p =
                (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
            
            //p爲最後一個元素,那麼他的後置節點必定是空
            p.after = null;

            //b爲e的前置元素,如果b爲空,說明此元素必定是鏈表的第一個元素,更新之後
            //鏈表的頭結點已經變成尾節點,那麼原鏈表的第二個節點就要變爲頭結點
            if (b == null)
                head = a;
            else
                //如果b不是空,那麼b的後置節點就由p變爲p的後置節點
                b.after = a;

            //如果p的後置節點不爲空,那麼更新後置節點a的前置節點爲b
            if (a != null)
                a.before = b;
            else
            //如果p的後置節點爲空,那麼p就是尾節點,那麼更新last的節點爲p的前置節點
                last = b;
            //如果原來的尾節點爲空,那麼原鏈表就只有一個元素
            if (last == null)
                head = p;
            else {
            //更新當前節點p的前置節點爲 原尾節點last, last的後置節點是p
                p.before = last;
                last.after = p;
            }

            //p爲最新的尾節點
            tail = p;

            //修改modCount
            ++modCount;
        }
    }

        在這裏需要注意兩點,一是調用次函數之後,訪問的這個元素會移動到雙向鏈表的尾端,二是在accessOrder=true的模式下,迭代LinkedHashMap時,如果同時查詢訪問數據,也會導致fail-fast,因爲迭代的順序已經改變。

  • 刪除操作

//如果key對應的value存在,則刪除這個鍵值對。 並返回value。如果不存在 返回null。

public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
}

        之後是removeNode函數,所有的刪除操作最後都需要執行到此函數

//刪除指定元素,value是指需要刪除的key對應的value,matchValue爲true表示必須
//key,value均相等纔可以刪除,如果movable爲false則刪除之後不移動其他節點

final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {

        //tab指向hashmap的數組,p爲待刪元素所在哈希桶的首個元素
        Node<K,V>[] tab; Node<K,V> p; int n, index;

        
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            
            //如果第一個節點就是需要刪除的節點。則將首節點賦值給node
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {

                //遍歷整個鏈表找到需要刪除的節點。賦值給node
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }

            //如果找到了對應的node且不爲空,match爲false或者value也一致則進行刪除
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    //如果node ==  p,說明是鏈表頭是待刪除節點
                    tab[index] = node.next;
                else
                    //如果是中間節點,進行刪除並修改前後順序
                    p.next = node.next;
                //修改modCount
                ++modCount;
                //size減一
                --size;
                //在hashmap中次函數爲空,LinkedHashMap重寫此方法
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

       以上就是hashmap的刪除操作,在linkedHashMap中還有afterNodeRemoval函數來刪除雙向鏈表中指定的元素。

 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;
        //如果b爲空,p就是頭節點,刪除之後a就是頭節點,否則將前置節點b的後置節點指向a
        if (b == null)
            head = a;
        else
            b.after = a;
        //如果a爲空,那麼p就是尾節點,刪除之後,b就是新的尾節點,否則a的前置節點就改爲b
        if (a == null)
            tail = b;
        else
            a.before = b;
    }
  • 遍歷操作

       一般通過keyset獲取key的set集合。

public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
            //在這裏linkedHashMap重寫了此函數。
            ks = new LinkedKeySet();
            keySet = ks;
        }
        return ks;
    }


    final class LinkedKeySet extends AbstractSet<K> {
        //鏈表元素個數
        public final int size()                 { return size; }
        public final void clear()               { LinkedHashMap.this.clear(); }

        //遍歷的目的是獲取Iterator
        public final Iterator<K> iterator() {
            return new LinkedKeyIterator();
        }
        //這個直接使用的hashmap的containsKey函數,最終使用的是getNode,前文已經講過就不在強調
        public final boolean contains(Object o) { return containsKey(o); }

        //remove最後也是調用了removeNode,前文已經講過。就不在強調
        public final boolean remove(Object key) {
            return removeNode(hash(key), key, null, false, true) != null;
        }
      
    }

我們再看看map中value的集合

 //此函數目的是獲取value的集合
public Collection<V> values() {
        Collection<V> vs = values;
        if (vs == null) {
            vs = new LinkedValues();
            values = vs;
        }
        return vs;
    }

    final class LinkedValues extends AbstractCollection<V> {
        public final int size()                 { return size; }
        public final void clear()               { LinkedHashMap.this.clear(); }
        //最重要的一點是獲取迭代器
        public final Iterator<V> iterator() {
            return new LinkedValueIterator();
        }
        //linkedHashMap重寫了contains函數,
        public final boolean contains(Object o) { return containsValue(o); }
        public final Spliterator<V> spliterator() {
            return Spliterators.spliterator(this, Spliterator.SIZED |
                                            Spliterator.ORDERED);
        }
       
    }

   //判斷是否含有此函數就是遍歷一下雙向鏈表
   public boolean containsValue(Object 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;
    }

看完這個之後,我們再看看entry的遍歷

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

    final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final int size()                 { return size; }
        public final void clear()               { LinkedHashMap.this.clear(); }
        
        //之後的遍歷都是通過Iterator來進行
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new LinkedEntryIterator();
        }
        public final boolean contains(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
            Object key = e.getKey();
            //通過getnode方法來獲取節點
            Node<K,V> candidate = getNode(hash(key), key);
            return candidate != null && candidate.equals(e);
        }
       
    }

通過以上的內容我們可以明白。無論是通過哪種方式遍歷,核心都在迭代器。無論哪個迭代器,最終都是調用到了LinkedEntryIterator

 abstract class LinkedHashIterator {
        //下一個節點
        LinkedHashMapEntry<K,V> next;
        //當前節點
        LinkedHashMapEntry<K,V> current;

        int expectedModCount;

        LinkedHashIterator() {
            //初始化的時候將next指向雙向鏈表的頭結點
            next = head;
            //記錄當前的modCount,與fail-fast有關
            expectedModCount = modCount;
            //current爲空
            current = null;
        }

        public final boolean hasNext() {
            return next != null;
        }

        //nextNode方法就是我們用到的next方法,
        //迭代LinkedHashMap,就是從內部維護的雙鏈表的表頭開始循環輸出
        final LinkedHashMapEntry<K,V> nextNode() {
            LinkedHashMapEntry<K,V> e = next;

            //如果查詢的時候,有其他對這個map的操作,導致改變了modCount,就造成fail-fast
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            //如果返回的是空,則拋出異常
            if (e == null)
                throw new NoSuchElementException();
            current = e;
            next = e.after;
            return e;
        }

        //刪除方法,就直接使用了hashmap的remove,前文已經介紹過。
        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;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }
  • 總結

1  linkedHashMap與hashmap的結構是一樣的,都是數組+鏈表+紅黑樹,他們的擴容機制也是一樣的。

2  linkedhashmap是通過雙向鏈表來維護數據的。與hashmap的拉鍊式存儲不一樣。

3 linkedhashmap存儲順序與添加順序是一樣得。同事可以根據accessOrder的值來決定是否移動元素達到lru的目的。

   最後針對一些網上的面試題做一些回答:

   

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章