6 手写Java LinkedHashMap 核心源码

九路
• 阅读 1316

概述

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的原理 6 手写Java LinkedHashMap 核心源码

LinkedHashMap中的节点如下图 6 手写Java 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中的链表用的是双向循环链表 如图下: 6 手写Java LinkedHashMap 核心源码

由上图可以知道,图中是一个双向链表,头节点和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的核心原理。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
2年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这