1. 程式人生 > >6 手寫Java LinkedHashMap 核心原始碼

6 手寫Java LinkedHashMap 核心原始碼

概述

LinkedHashMap是Java中常用的資料結構之一,安卓中的LruCache快取,底層使用的就是LinkedHashMap,LRU(Least Recently Used)演算法,即最近最少使用演算法,核心思想就是當快取滿時,會優先淘汰那些近期最少使用的快取物件

LruCache的快取演算法

LruCache採用的快取演算法為LRU(Least Recently Used),最近最少使用演算法。核心思想是當快取滿時,會首先把那些近期最少使用的快取物件淘汰掉

LruCache的實現

LruCache底層就是用LinkedHashMap來實現的。提供 get 和 put 方法來完成物件的新增和獲取

LinkedHashMap與HashMap的區別

相同點:
1. 都是key,value進行新增和獲取
2. 底層都是使用陣列來存放資料
 
不同點:
1. HashMap是無序的,LinkedHashMap是有序的(插入順序和訪問順序)
2. LinkedHashMap記憶體的節點存放在資料中,但是節點內部有兩個指標,來完成雙向連結串列的操作,來保證節點插入順序或者訪問順序

LinkedHashMap的使用

LinkedHashMap插入順序的演示程式碼

    public static void main(String[] args) {

        //預設記錄的就是插入順序
        Map<String, String> map = new LinkedHashMap<>();
        map.put("name", "tom");
        map.put("age", "34");
        map.put("address", "beijing");

        Iterator iterator = map.entrySet().iterator();

        //遍歷
        while (iterator.hasNext()) {
            Map.Entry entry = (Map.Entry) iterator.next();
            String key = (String) entry.getKey();
            String value = (String) entry.getValue();
            System.out.println("Key = " + key + ", Value = " + value);
        }
    }

輸出如下:

Key = name, Value = tom
Key = age, Value = 34
Key = address, Value = beijing

由輸出可以看到
我們往LinedHashMap中分別按順序插入了name,age,address以及對應的value
遍歷的時候,也是按順序分別輸出了 name,age,address以及對應的value
所以可以,LinkedHashMap預設記錄的就是插入的順序

作為比較,我們再看來一下 HashMap 的遍歷是不是有序的。就以上面這幾個值為例
程式碼以及輸出如下:

HashMap的遍歷

  public static void main(String[] args) {

        //插入和上面一樣的值
        Map<String, String> map = new HashMap<>();
        map.put("name", "tom");
        map.put("age", "34");
        map.put("address", "beijing");

        Iterator iterator = map.entrySet().iterator();

        //遍歷
        while (iterator.hasNext()) {
            Map.Entry entry = (Map.Entry) iterator.next();
            String key = (String) entry.getKey();
            String value = (String) entry.getValue();
            System.out.println("Key = " + key + ", Value = " + value);
        }
    }

輸出如下

Key = address, Value = beijing
Key = name, Value = tom
Key = age, Value = 34

從上面可以得知:

  1. HashMap遍歷的時候,是無序的,和插入的順序是不相關的。
  2. LinkedHashMap預設的順序是記錄插入順序

既然LinkedHashMap預設是按著插入順序的,那麼肯定也有其它的順序。
對的,LinkedHashMap還可以記錄訪問的順序。訪問過的元素放在連結串列前面
遍歷的時候最近訪問的元素最後才遍歷到

LinkedHashMap的訪問順序的演示程式碼

    public static void main(String[] args) {

        /**
         * 第一個引數:陣列的大小
         * 第二個引數:擴容因子,和HashMap一樣,新增的元素的個數達到 16 * 0.75時,開始擴容
         * 第三個引數:true:表示記錄訪問順序
         *           false:表示記錄插入順序(預設的順序)
         */
        Map<String, String> map = new LinkedHashMap<>(16,0.75f,true);

        //分別插入下面幾個值
        map.put("name", "tom");
        map.put("age", "34");
        map.put("address", "beijing");

        //既然演示訪問順序,我們就訪問其中一個元素,這裡只是列印一下
        System.out.println("我是被訪問的元素:" + map.get("age"));

        //訪問完後,我們再遍歷,注意輸出的順序
        Iterator iterator = map.entrySet().iterator();
        //遍歷
        while (iterator.hasNext()) {
            Map.Entry entry = (Map.Entry) iterator.next();
            String key = (String) entry.getKey();
            String value = (String) entry.getValue();
            System.out.println("Key = " + key + ", Value = " + value);
        }
    }

輸出如下:

我是被訪問的元素:34
Key = name, Value = tom
Key = address, Value = beijing
Key = age, Value = 34

從上面可以得知:
插入元素完成之後,我們訪問了age,並列印其值
之後再遍歷,因為記錄的是訪問順序,LinkedHashMap會把最近使用的元素放到最後面,所以遍歷的時候,本來age是第二次插入的,但是遍歷的時候,卻是最後一個遍歷出來的。

LruCache就是利用了LinkedHashMap的這種性質,最近使用的元素都放在最後
最近不使用的元素自然就在前面,所以快取滿了的時候,刪除前面的。新新增元素的時候放在最後面

LinkedHashMap的原理

LinkedHashMap,望文生義 Link + HashMap,Link就連結串列
所以LinkedHashMap就是底層使用陣列存放元素,使用連結串列維護插入元素的順序

使用一張圖來說明LinkedHashMap的原理

LinkedHashMap中的節點如下圖

下面我們就來手寫這樣一個結構QLinkedHashMap

首先定義節點的結構,我們就叫QEntry,如下

  static class QEntry<K, V> {
        public K key;      //key
        public V value;    //value
        public int hash;   //key對應的hash值
        public QEntry<K, V> next;   //hash衝突時,構成一個單鏈表

        public QEntry<K, V> before; //當前節點的前一個節點
        public QEntry<K, V> after;  //當前節點的後一個節點


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

        //刪除當前節點
        private void remove() {
            //當前節點的上一個節點的after指向當前節點的下一個節點
            this.before.after = after;
            //當前節點的下一個節點的before指向當前節點的上一個節點
            this.after.before = before;
        }

        //將當前節點插入到existingEntry節點之前
        private void addBefore(QEntry<K, V> existingEntry) {

            //插入到existingEntry前,那麼當前節點後一個節點指向existingEntry
            this.after = existingEntry;

            //當前節點的上一個節點也需要指向existingEntry節點的上一個節點
            this.before = existingEntry.before;

            //當前節點的下一個節點的before也得指向自己
            this.after.before = this;

            //當前節點的上一個節點的after也得指向自己
            this.before.after = this;
        }

        //訪問了當前節點時,會呼叫這個函式
        //在這裡面就會處理訪問順序和插入順序
        void recordAccess(QLinkedHashMap<K, V> m) {
            QLinkedHashMap<K, V> lm = (QLinkedHashMap<K, V>) m;

            //如果accessOrder為true,也就是訪問順序
            if (lm.accessOrder) {

                //把當前節點從連結串列中刪除
                remove();

                //再把當前節點插入到雙向連結串列的尾部
                addBefore(lm.header);
            }
        }
    }

我們的QLinkedHashMap中的連結串列用的是雙向迴圈連結串列
如圖下:

由上圖可以知道,圖中是一個雙向連結串列,頭節點和A,B兩個連結串列
其中
header.before指向最後一個節點
header.after 指向header節點

其中 QLinkedHashMap的大部分程式碼和手寫Java HashMap核心原始碼 一樣。
可以先看看手寫Java HashMap核心原始碼一章節

QLinkedHashMap全部原始碼以及註釋如下:

public class QLinkedHashMap<K, V> {
    private static int DEFAULT_INITIAL_CAPACITY = 16;   //預設陣列的大小
    private static float DEFAULT_LOAD_FACTOR = 0.75f;   //預設的擴容因子

    private QEntry[] table;     //底層的陣列
    private int size;           //數量


    //下面這兩個屬性是給連結串列用的

    //true:表示按著訪問的順序儲存   false:按照插入的順序儲存(預設的方式)
    private boolean accessOrder;

    //雙向迴圈連結串列的表頭,記住,這裡只有一個頭指標,沒有尾指標
    //所以需要用迴圈連結串列來實現雙向連結串列
    //即:從前可以往後遍歷,也可以從後往前遍歷
    private QEntry<K, V> header;


    public QLinkedHashMap() {
        //建立DEFAULT_INITIAL_CAPACITY大小的陣列
        table = new QEntry[DEFAULT_INITIAL_CAPACITY];
        size = 0;

        //預設按照插入的順序儲存
        accessOrder = false;

        //初始化
        init();
    }

    /**
     *
     * @param capcacity     陣列的大小
     * @param accessOrder   按照何種順序儲存
     */
    public QLinkedHashMap(int capcacity, boolean accessOrder) {
        table = new QEntry[capcacity];
        size = 0;

        this.accessOrder = accessOrder;
        init();
    }

    //這裡主要是初始化雙向迴圈連結串列
    private void init() {
        //新建一個表頭
        header = new QEntry<>(null, null, -1, null);

        //連結串列為空的時候,只有一個頭節點,所以頭節點的下一個指向自己,上一個節點也指向自己
        header.after = header;
        header.before = header;
    }

    //插入一個鍵值對
    public V put(K key, V value) {
        if (key == null)
            throw new IllegalArgumentException("key is null");

        //拿到key的hash值
        int hash = hash(key.hashCode());
        //存在陣列中的哪個位置
        int i = indexFor(hash, table.length);

        //看看有沒有key是一樣的,如果有,替換掉舊掉,把新值儲存起來
        //如呼叫了兩次 map.put("name","tom");
        // map.put("name","jim"); ,那麼最新的name對應的value就是jim
        QEntry<K, V> e = table[i];
        while (e != null) {
            //檢視有沒有相同的key,如果有就儲存新值,返回舊值
            if (e.hash == hash && (key == e.key || key.equals(e.key))) {
                V oldValue = e.value;
                e.value = value;

                //重點就是這一句,找到了相同的節點,也就是訪問了一次
                //如果accessOrder是true,就要把這個節點放到連結串列的尾部
                e.recordAccess(this);

                //返回舊值
                return oldValue;
            }

            //繼續下一個迴圈
            e = e.next;
        }

        //如果沒有找到與key相同的鍵
        //新建一個節點,放到當前 i 位置的節點的前面
        QEntry<K, V> next = table[i];
        QEntry newEntry = new QEntry(key, value, hash, next);

        //儲存新的節點到 i 的位置
        table[i] = newEntry;

        //把新節點新增到雙向迴圈連結串列的頭節點的前面,
        //記住,新增到header的前面就是新增到連結串列的尾部
        //因為這是一個雙向迴圈連結串列,頭節點的before指向連結串列的最後一個節點
        //連結串列的最後一個節點的after指向header節點
        //剛開始我也以為是新增到了連結串列的頭部,其實不是,是新增到了連結串列的尾部
        //這點可以參考圖好好想想
        newEntry.addBefore(header);

        //別忘了++
        size++;

        return null;
    }

    //根據key獲取value,也就是對節點進行訪問
    public V get(K key) {

        //同樣為了簡單,key不支援null
        if (key == null) {
            throw new IllegalArgumentException("key is null");
        }

        //對key進行求hash值
        int hash = hash(key.hashCode());

        //用hash值進行對映,得到應該去陣列的哪個位置上取資料
        int index = indexFor(hash, table.length);

        //把index位置的元素儲存下來進行遍歷
        //因為e是一個連結串列,我們要對連結串列進行遍歷
        //找到和key相等的那個QEntry,並返回value
        QEntry<K, V> e = table[index];
        while (e != null) {

            //看看陣列中是否有相同的key
            if (hash == e.hash && (key == e.key || key.equals(e.key))) {

                //訪問到了節點,這句很重要,如果有相同的key,就呼叫recordAccess()
                e.recordAccess(this);

                //返回目標節點的值
                return e.value;
            }

            //繼續下一個迴圈
            e = e.next;
        }

        //沒有找到
        return null;
    }

    //返回一個迭代器類,遍歷用
    public QIterator iterator(){
        return new QIterator(header);
    }

    //根據 h 求key落在陣列的哪個位置
    static int indexFor(int h, int length) {
        //或者  return h & (length-1) 效能更好
        //這裡我們用最容易理解的方式,對length取餘數,範圍就是[0,length - 1]
        //正好是table陣列的所有的索引的範圍

        h = h > 0 ? h : -h; //防止負數

        return h % length;
    }

    //對hashCode進行運算,JDK中HashMap的實現,直接拷貝過來了
    static int hash(int h) {
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    //定義一個迭代器類,方便遍歷用
    public class QIterator {
        QEntry<K,V> header; //表頭
        QEntry<K,V> p;

        public QIterator(QEntry header){
            this.header = header;
            this.p = header.after;
        }

        //是否還有下一個節點
        public boolean hasNext() {
            //當 p 不等於 header的時候,說明還有下一個節點
            return p != header;
        }

        //如果有下一個節點,獲取之
        public QEntry next() {
            QEntry r = p;
            p = p.after;
            return r;
        }
    }

    static class QEntry<K, V> {
        public K key;      //key
        public V value;    //value
        public int hash;   //key對應的hash值
        public QEntry<K, V> next;   //hash衝突時,構成一個單鏈表

        public QEntry<K, V> before; //當前節點的前一個節點
        public QEntry<K, V> after;  //當前節點的後一個節點


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

        //刪除當前節點
        private void remove() {
            //當前節點的上一個節點的after指向當前節點的下一個節點
            this.before.after = after;
            //當前節點的下一個節點的before指向當前節點的上一個節點
            this.after.before = before;
        }

        //將當前節點插入到existingEntry節點之前
        private void addBefore(QEntry<K, V> existingEntry) {

            //插入到existingEntry前,那麼當前節點後一個節點指向existingEntry
            this.after = existingEntry;

            //當前節點的上一個節點也需要指向existingEntry節點的上一個節點
            this.before = existingEntry.before;

            //當前節點的下一個節點的before也得指向自己
            this.after.before = this;

            //當前節點的上一個節點的after也得指向自己
            this.before.after = this;
        }

        //訪問了當前節點時,會呼叫這個函式
        //在這裡面就會處理訪問順序和插入順序
        void recordAccess(QLinkedHashMap<K, V> m) {
            QLinkedHashMap<K, V> lm = (QLinkedHashMap<K, V>) m;

            //如果accessOrder為true,也就是訪問順序
            if (lm.accessOrder) {

                //把當前節點從連結串列中刪除
                remove();

                //再把當前節點插入到雙向連結串列的尾部
                addBefore(lm.header);
            }
        }
    }
}

上面就是QLinkedHashMap的全部原始碼。
擴容以及刪除功能我們沒有寫,有興趣的讀者可以自己實現一下。

我們寫一段程式碼來測試,如下:

 public static void main(String[] args){
        //新建一個預設的建構函式,預設是按照插入順序儲存
        QLinkedHashMap<String,String> map = new QLinkedHashMap<>();
        map.put("name","tom");
        map.put("age","32");
        map.put("address","beijing");

        //驗證是不是按照插入的順序列印
        QLinkedHashMap.QIterator iterator =  map.iterator();
        while (iterator.hasNext()){
            QEntry e = iterator.next();
            System.out.println("key=" + e.key + " value=" + e.value);
        }
    }

輸出如下:

key=name value=tom
key=age value=32
key=address value=beijing

可以看到我們輸出的時候,是按照插入的順序輸出的。

我們再稍微改一下程式碼,只需要改QLinkedHashMap的建構函式即可
程式碼如下:

  public static void main(String[] args){
        //新建一個大小為16,順序是訪問順序的 map
        QLinkedHashMap<String,String> map = new QLinkedHashMap<>(16,true);

        //分別插入以下鍵值對
        map.put("name","tom");
        map.put("age","32");
        map.put("address","beijing");

        //訪問其中一個元素,這裡什麼也不做
        //訪問了age,那麼列印的時候,age應該是最後一個列印的
        map.get("age");


        //驗證是不是按照訪問順序列印,age是不是最後一個列印
        QLinkedHashMap.QIterator iterator =  map.iterator();
        while (iterator.hasNext()){
            QEntry e = iterator.next();
            System.out.println("key=" + e.key + " value=" + e.value);
        }
    }

輸出如下:

key=name value=tom
key=address value=beijing
key=age value=32

由上可以,我們實現了可以以插入順序和訪問順序的HashMap。
雖然沒有實現擴容機制和刪除。但足以提示QLinkedHashMap的核心原理。