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的核心原理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM