1. 程式人生 > >LinkedList,LinkedHashMap,LruCache原始碼解析

LinkedList,LinkedHashMap,LruCache原始碼解析

最近正好在複習資料結構的知識,順帶看了下jdk 1.8中的LinkedList和LinkedHashMap以及android中常用的LruCache的原始碼(內部採用LinkedHashMap實現),以加強自己的理解,下面就分享一下我閱讀原始碼的一些簡單的心得。

一、簡單高效的雙鏈表LinkedList

為什麼使用雙鏈表而不使用單鏈表,原因應該是,作為一種需要頻繁在表頭或表尾進行插入或刪除操作的資料結構,選用雙鏈表的效率會比單鏈表要高。試想一下,如果要刪除單鏈表的表尾節點,除了需要將最後一個節點置空,還需要將該節點的上一個節點的next域置為null,因為此時無法直接通過最後一個節點得到倒數第二個節點的位置,所以只能重新從表頭開始遍歷,時間複雜度為O(n),而如果是雙鏈表的話,可以直接通過最後一個節點的prev域即前驅節點得到它上一個節點,然後再將其next域置空,時間複雜度為O(1)。所以,雙鏈表的優勢就是,增加或刪除節點的速度較快,尤其是在表尾節點。

原始碼
先來看下節點類的定義

private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

很簡單,Node類是一個靜態內部類,包含了資料部分和兩個引用,分別指向前驅節點和後繼節點,在節點類構造的時候分別指定它的前驅節點,資料域和後繼節點。這樣的建構函式,在後面進行插入或刪除操作的時候給我們省去了很多麻煩。

在表頭插入

/**
     * Links e as first element.
     */
    private void linkFirst(E e) {
        final Node<E> f = first;
        final Node<E> newNode = new Node<>(null, e, f);
        first = newNode;
        if
(f == null) last = newNode; else f.prev = newNode; size++; modCount++; }

我們來分析一下,也不是很複雜,first是LinkedList的成員變數,即連結串列的頭指標,該方法首先先儲存原來的頭結點,賦值給一個新的Node類f,然後建立新的節點,資料為e,前驅節點為null(因為要做新的第一個嘛),後繼節點是f,接著將頭指標指向新建立的節點。這樣就成功地將新節點插入到了原頭結點的前面,但由於是雙鏈表,插入刪除時需要調整兩部分的指標,我們還要將原來頭結點(f)的prev域設為新的頭結點,在這之前,先判斷一下原來的頭結點是不是為null,如果為null的話,說明原來的雙鏈表是空表,現在插入的是第一個節點,所以last尾指標也設為newNode。最後增加連結串列的size。

在表尾插入

/**
     * Links e as last element.
     */
    void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

與在表頭插入很類似,先儲存原先尾節點的值為l,然後建立新的尾節點插入到l後面,然後調整原先尾節點l的next域,指向新的尾節點。在這之前同樣先判斷一下是不是空表,是空表的話,插入一個節點後first頭結點也指向newNode。

在一個節點之前插入

/**
     * Inserts element e before non-null Node succ.
     */
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev;
        final Node<E> newNode = new Node<>(pred, e, succ);
        succ.prev = newNode;
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }

該方法將一個新節點插入到succ節點前面。邏輯也很清晰,首先先獲取到succ的前驅節點pred,然後新建立一個節點插入到pred和succ兩者之間,然後分別修改succ的prev域和pred的next域,都指向新的節點。同樣的,在修改pred的next域之前,判斷pred是否為空,如果為空,說明原來succ是頭結點,所以要把頭指標指向新建立的節點newNode。

在表頭刪除

 /**
     * Unlinks non-null first node f.
     */
    private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        final E element = f.item;
        final Node<E> next = f.next;
        f.item = null;
        f.next = null; // help GC
        first = next;
        if (next == null)
            last = null;
        else
            next.prev = null;
        size--;
        modCount++;
        return element;
    }

在刪除表頭節點f的時候,先儲存其下一個節點next,接著將f的資料域和指標域強制置為null,這樣可以幫助垃圾收集器GC很快的回收這兩個引用。跟著將新的頭指標指向next,然後判斷next是否為空,如果next為空,說明原來只有一個節點,刪除後表變空了,所以將last也設為null,否則的話將next(此時的新頭結點)的prev前驅指標設為null,最後修改表的長度大小,並將刪除的頭結點的值返回。

在表尾刪除

/**
     * Unlinks non-null last node l.
     */
    private E unlinkLast(Node<E> l) {
        // assert l == last && l != null;
        final E element = l.item;
        final Node<E> prev = l.prev;
        l.item = null;
        l.prev = null; // help GC
        last = prev;
        if (prev == null)
            first = null;
        else
            prev.next = null;
        size--;
        modCount++;
        return element;
    }

邏輯與上面在表頭刪除正好相反,就不在贅述了。

在表中刪除

/**
     * Unlinks non-null node x.
     */
    E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;

        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }

        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }

        x.item = null;
        size--;
        modCount++;
        return element;
    }

從雙鏈表中刪除指定結點x,首先獲得x的前驅和後繼節點,如果前驅節點為null,說明x為頭結點,刪除後直接將頭指標指向x的後繼節點,如果不是頭結點則將x的前驅節點的後繼指向x的後繼,並將x的前驅置為空,將x從鏈中斷開,此處畫個圖就很好理解;接著同樣判斷x的後繼next是不是空,如果是空說明x是尾節點,要刪除的話則直接將尾指標指向x的前驅,否則修改x後繼節點的前驅指標,指向x的前驅,再把x的next置為null,將x從鏈中斷開。

我們常用的一些add和remove操作,呼叫的都是上面的函式。

public boolean add(E e) {
        linkLast(e);
        return true;
    }
public void addFirst(E e) {
        linkFirst(e);
    }
 public void addLast(E e) {
        linkLast(e);
    }
public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }
public E removeLast() {
        final Node<E> l = last;
        if (l == null)
            throw new NoSuchElementException();
        return unlinkLast(l);
    }
public void push(E e) {
        addFirst(e);
    }
 public E pop() {
        return removeFirst();
    }

如果你理解了雙鏈表的基本插入刪除操作,那麼LinkedList的原始碼你也可以差不多基本理解了,剩下的一些細節我就不再說了,下面看LinkedHashMap。

二、LinkedHashMap

LinkedHashMap是HashMap的子類,通俗的講就是加了雙鏈表結構的HashMap。HashMap大家都很清楚,本質就是Entry陣列加連結串列(或者紅黑樹)的形式,Entry這個資料結構包括hash值,key-value鍵值對,和next索引(通過鏈地址法用來解決雜湊衝突)。而我們看看LinkedHashMap裡的Entry

LinkedHashMap的Entry

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

可以看到LinkedHashMap在原來HashMap的Entry的基礎上又增加了before和after兩個指標(java中只有引用,這裡說指標是為了方便理解),分別指向前驅和後繼節點,所以說,它是一個完完全全的雙鏈表+HashMap。

雙鏈表表頭與表尾的定義

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

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

還有一個很重要的成員變數accessOrder

final boolean accessOrder;

如果accessOrder為true表明LinkedHashMap按照訪問的順序來迭代,如果為false表明LinkedHashMap按照插入的順序來迭代。預設是按照插入順序來遍歷:

 public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }

在建立新節點的時候,是直接將Entry加入到雙鏈表的尾部:

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

下面我們就來看一下這個linkNodeLast()方法

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

linkNodeLast方法將一個Entry節點加入到雙鏈表的尾部。首先儲存原雙鏈表的表尾節點tail為last,然後將tail指向新插入的節點p,此時判斷原來的表尾節點last是否為null,如果為null,說明原來雙鏈表為空表,插入後只有一個節點,所以將head頭指標也指向p,否則的話,將p的前驅指向原來的表尾節點last,將原來表尾節點last的後繼指向新的表尾節點p。

細想一下,和LinkedList那段是不是很像?沒錯,因為歸根結底還是雙鏈表的插入操作。

下面看一下LinkedHashMap的get()方法

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

可以看到,在訪問完一個節點後,如果accessOrder為true的話(即設定按照訪問順序來迭代),會呼叫afterNodeAccess()函式將剛訪問過的節點放置到雙鏈表的尾部,即放到最新的位置,代表這個節點剛被訪問過。我們再去afterNodeAccess()函式看看究竟

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)       //如果p是表頭
                head = a;        //將e從表中斷開後,表頭指向e的後繼
            else
                b.after = a;     //否則將e從表中斷開
            if (a != null)
                a.before = b;
            else                 //p是表尾
                last = b;        //將e從表中斷開後,表尾指向e的前驅
            if (last == null)    //如果原連結串列是空表
                head = p;        //表頭指向p
            else {               //否則插向原連結串列的表尾
                p.before = last; 
                last.after = p;
            }
            tail = p;            //尾節點指向新插入的節點
            ++modCount;
        }
    }

該函式將節點e從原雙鏈表中摘下,並插入到最後的位置。這裡還是先用一個last來儲存原來的tail表尾節點,如果accessOrder為true,並且此時節點p(由e轉換而來)並不在表尾,則執行後面的操作,後面的操作可以分為兩部分,第一部分是將節點p從原來雙鏈表的位置中斷開,第二部分是將節點p插入到表尾。和之前在LinkedList中的操作很類似,將節點p從原連結串列中刪除時,判斷了p是否在表頭或是在表尾(與LinkedList的unlinkFirst和unlinkLast函式相同);將p插入到連結串列尾部時,加入了表是否為空的判斷(與LinkedList的linkLast函式相同)。

綜上,我們可以看到,將LinkedList中雙鏈表的增加和刪除操作與HashMap相結合,就是LinkedHashMap。

三、LruCache

理解了LinkedHashMap,就不難理解LruCache的實現原理了。這個Android中最常用的快取類,內部就維護了一個LinkedHashMap的引用

private final LinkedHashMap<K, V> map;

在 LruCache初始化時,指定了hasmap的擴容因子,並設定accessOrder為true,按訪問順序迭代,來達到LRU(最近最久未使用)演算法的效果:最近被訪問的,或者最新插入的,總是在表尾,而不怎麼被經常訪問的,就會逐漸向表頭移動,此時就可以從表頭將這些不常用的快取淘汰。

我們來看一下從快取中取資料的get方法

public final V get(K key) {
    if (key == null) {
        throw new NullPointerException("key == null");
    }

    V mapValue;
    synchronized (this) {
     // 如果根據相應的key能查詢到value,就增加一次快取命中的次數hitCount,並且返回結果
        mapValue = map.get(key);
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
     // 否則增加一次未命中次數missCount
        missCount++;
    }

    /*
     * Attempt to create a value. This may take a long time, and the map
     * may be different when create() returns. If a conflicting value was
     * added to the map while create() was working, we leave that value in
     * the map and release the created value.
     */
    V createdValue = create(key);
    if (createdValue == null) {
        return null;
    }

    synchronized (this) {
        createCount++;
     // 如果我們重寫了create(key)方法而且返回值不為空,那麼將上述的key與這個返回值寫入到map當中
        mapValue = map.put(key, createdValue);

        if (mapValue != null) {
            // There was a conflict so undo that last put
       // 方法放入最後put的key,value值
            map.put(key, mapValue);
        } else {
            size += safeSizeOf(key, createdValue);
        }
    }

    if (mapValue != null) {
     // 這個方法也可以重寫
        entryRemoved(false, key, createdValue, mapValue);
        return mapValue;
    } else {
        trimToSize(maxSize);
        return createdValue;
    }
}

下面再看一下LruCache更新快取的策略,主要在trimToSize()這個函式中

 /**
     * Remove the eldest entries until the total of remaining entries is at or
     * below the requested size.
     *
     * @param maxSize the maximum size of the cache before returning. May be -1
     *            to evict even 0-sized elements.
     */
    public void trimToSize(int maxSize) {
        while (true) {
            K key;
            V value;
            synchronized (this) {
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(getClass().getName()
                            + ".sizeOf() is reporting inconsistent results!");
                }

                if (size <= maxSize || map.isEmpty()) {
                    break;
                }
                //按照訪問順序來迭代,最新訪問過的都在表尾,表頭的是最近長時間內都沒有使用過的快取
                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                //將快取移除
                map.remove(key);
                //修改快取連結串列的size
                size -= safeSizeOf(key, value);
                //淘汰掉一個快取
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }

從註釋中就可以看到,這個函式將最老的也就是最久沒有訪問過的entry刪除,以將整體entry的容量降低到指定大小(淘汰了最近不常用的快取)。可以看到,它通過map.entrySet().iterator()獲得LinkedHashMap的迭代器,從儲存了快取資料的雙鏈表的第一個節點開始(即最久沒有使用過的快取,因為最近剛使用過的快取都移到了表尾),逐個呼叫remove函式,將其從表中刪除,以降低整體快取的大小。

看到了這裡,是不是對LinkedList,LinkedHashMap,LruCache的基本原理有了一個清楚的認識呢?我們看到,萬變不離其宗,其重點就是圍繞雙鏈表的增刪改操作,資料結構的基礎確實很重要。

相關推薦

LinkedListLinkedHashMapLruCache原始碼解析

最近正好在複習資料結構的知識,順帶看了下jdk 1.8中的LinkedList和LinkedHashMap以及android中常用的LruCache的原始碼(內部採用LinkedHashMap實現),以加強自己的理解,下面就分享一下我閱讀原始碼的一些簡單的心得。

LRU演算法以及Apache LRUMap原始碼解析

1. 什麼是LRU LRU(least recently used) : 最近最少使用 LRU就是一種經典的演算法,在容器中,對元素定義一個最後使用時間,當新的元素寫入的時候,如果容器已滿,則淘汰最近最少使用的元素,把新的元素寫入。 1.1 自定義實現LRU的要求 比

2018.9.30學習筆記(MapHashMapLinkedHashMapTreeMap)

1 Map 單列集合底層是雙列集合 2 Map的基本方法 package com.haidai.Map; import java.util.HashMap; import java.ut

HashMapLinkedHashMapTreeMap區別

注:去年專案有用到LinkedHashMap,最開始忘記這個,浪費了點時間,剛好看到阿里開發手冊,記錄一下區別。 HashMap 最多隻允許一條記錄的鍵為Null;允許多條記錄的值為 Null 非執行緒安全 LinkedHashMap 儲存了記錄的插入

Android——LruCache原始碼解析

以下針對 Android API 26 版本的原始碼進行分析。 在瞭解LruCache之前,最好對LinkedHashMap有初步的瞭解,LruCache的實現主要藉助LinkedHashMap。LinkedHashMap的原始碼解析,可閱讀Java——LinkedHashMap原始碼解析 概述 &nbs

LruCache原始碼解析

LruCache 之前分析過Lru演算法的實現方式:HashMap+雙向連結串列,參考連結:LRU演算法&&LeetCode解題報告 這裡主要介紹Android SDK中LruCache快取演算法的實現, 基於Android5.1版本原始碼.

集合ArrayList,LinkedList,HashMap,LinkedHashMap,ConcurremtHashMap分別的總結volatile 關鍵字的使用

1    集合 1.1    List 1.1.1    ArrayList      動態陣列     實現list介面,非同步 &nbs

==和equals的區別原始碼解析

這是Object類裡面的equals方法原始碼: public boolean equals(Object obj) { return (this == obj); } 看著還是用==在比較,原因分析:在大部分的封裝類中,都重寫了Object類的這個方法,

fastjson 始終將 null 物件以 "null " 的形式返回到前端引發的原始碼解析 - 下:來到 fasjson 內部消除疑惑

接上篇:fastjson 始終將 null 物件以 "null " 的形式返回到前端引發的原始碼解析 - 上:從 DispatcherServlet 出發 終於來到了 fastjson 內部。 FastJsonHttpMessageConverter 內部又呼叫了一系列的方法

原始碼解析關於java阻塞容器:ArrayBlockingQueueLinkedBlockingQueue等

Java的阻塞容器介紹JDK18 一ArrayBlockingQueue 類的定義 重要的成員變數 初始化 一些重要的非公開的方法

java 集合類 底層原始碼解析慢速更新~偏新手

我決定從java底層原始碼開始自己的部落格之旅,水平有限,很有可能寫的不對,歡迎大家指出缺點~部落格慢速保持更新! 先從java最常用的集合類開始更新吧~ java的集合類均來自於 java.util包下 java單列頂層介面 Collection 先看看該介面的定義: pub

LeetCode題目原始碼解析

leetcode_java 前言 刷leetcode的記錄,程式碼並不是十分滴優秀~~網上優秀滴題解也非常多…嗯…但還是想放上來,給自己看!嗯…很調皮! 介紹 大多題目的解題思路在程式碼中都有註釋,點選語言一欄中的Java, 跳轉到相對應的程式碼,github會實時更新,cs

原始碼系列SpringMybatisSpringbootNetty原始碼深度解析-Spring的整體架構與容器的基本實現-mybatis原始碼深度解析與最佳實踐

6套原始碼系列Spring,Mybatis,Springboot,Netty原始碼深度解析視訊課程   6套原始碼套餐課程介紹: 1、6套精品是掌櫃最近整理出的最新課程,都是當下最火的技術,最火的課程,也是全網課程的精品;   2、6套資源包含:全套完整

mybatis原始碼-解析配置檔案(三)之配置檔案Configuration解析(超詳細 值得收藏)

1. 簡介 1.1 系列內容 本系列文章講解的是mybatis解析配置檔案內部的邏輯, 即 Reader reader = Resources.getResourceAsReader("mybatis-config.xml"); SqlSessionFact

Laravel原始碼解析之入口程式設計師必學

前言 提升能力的方法並非使用更多工具,而是解刨自己所使用的工具。今天我們從Laravel啟動的第一步開始講起。 入口檔案 laravel是單入口框架,所有請求必將經過index.php define(‘LARAVEL_START’, microtime(true

Spring的IOC容器原始碼學習----Bean的定位載入解析註冊注入

目錄 IOC容器系列包含BeanFactory和ApplicationContext,這兩個介面就是IOC的具體表現形式。 他們的介面關係設計圖如下所示: 主要介面設計主線:    1.BeanFactory -> Hierarc

JDK8 HashMap原始碼解析一篇文章徹底讀懂HashMap

    在秋招面試準備中博主找過很多關於HashMap的部落格,但是秋招結束後回過頭來看,感覺沒有一篇全面、通俗易懂的講解HashMap文章(可能是博主沒有找到),所以在秋招結束後,寫下了這篇文章,盡最大的努力把HashMap原始碼講解的通俗易懂,並且儘量涵蓋面試中HashM

JAVA架構師VIP精品教程Spring原始碼解析視訊

JAVA架構師VIP精品課程 ,某泡學院稀缺資源, 《重磅乾貨Java架構師全套vip課程視訊教程》2018年最新視訊教程 java架構師篇,從學習計劃到java原始碼分析,

jdk8原始碼解析第一天(簡化版只記自己理解的要點)

1.String類: 實現了serilizable,comparable介面,seriliazable僅用於標誌,comparable的comparableTo方法用於比較字串大小。 底層是通過final char[] 實現字串的,其所有方法均是用字元陣列相關方法實現的。

Android事件分發機制完全解析帶你從原始碼的角度徹底理解(上)-郭霖

其實我一直準備寫一篇關於Android事件分發機制的文章,從我的第一篇部落格開始,就零零散散在好多地方使用到了Android事件分發的知識。也有好多朋友問過我各種問題,比如:onTouch和onTouchEvent有什麼區別,又該如何使用?為什麼給ListView引入了一