概述

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

6 手写Java LinkedHashMap 核心源码的更多相关文章

  1. 3 手写Java HashMap核心源码

    手写Java HashMap核心源码 上一章手写LinkedList核心源码,本章我们来手写Java HashMap的核心源码. 我们来先了解一下HashMap的原理.HashMap 字面意思 has ...

  2. 1 手写Java ArrayList核心源码

    手写ArrayList核心源码 ArrayList是Java中常用的数据结构,不光有ArrayList,还有LinkedList,HashMap,LinkedHashMap,HashSet,Queue ...

  3. 2 手写Java LinkedList核心源码

    上一章我们手写了ArrayList的核心源码,ArrayList底层是用了一个数组来保存数据,数组保存数据的优点就是查找效率高,但是删除效率特别低,最坏的情况下需要移动所有的元素.在查找需求比较重要的 ...

  4. 5 手写Java Stack 核心源码

    Stack是Java中常用的数据结构之一,Stack具有"后进先出(LIFO)"的性质. 只能在一端进行插入或者删除,即压栈与出栈 栈的实现比较简单,性质也简单.可以用一个数组来实 ...

  5. 4.1 手写Java PriorityQueue 核心源码 - 原理篇

    本章先讲解优先级队列和二叉堆的结构.下一篇代码实现 从一个需求开始 假设有这样一个需求:在一个子线程中,不停的从一个队列中取出一个任务,执行这个任务,直到这个任务处理完毕,再取出下一个任务,再执行. ...

  6. 4.2 手写Java PriorityQueue 核心源码 - 实现篇

    上一节介绍了PriorityQueue的原理,先来简单的回顾一下 PriorityQueue 的原理 以最大堆为例来介绍 PriorityQueue是用一棵完全二叉树实现的. 不但是棵完全二叉树,而且 ...

  7. Android版数据结构与算法(五):LinkedHashMap核心源码彻底分析

    版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 上一篇基于哈希表实现HashMap核心源码彻底分析 分析了HashMap的源码,主要分析了扩容机制,如果感兴趣的可以去看看,扩容机制那几行最难懂的 ...

  8. Java HashMap 核心源码解读

    本篇对HashMap实现的源码进行简单的分析. 所使用的HashMap源码的版本信息如下: /* * @(#)HashMap.java 1.73 07/03/13 * * Copyright 2006 ...

  9. 手撕spring核心源码,彻底搞懂spring流程

    引子 十几年前,刚工作不久的程序员还能过着很轻松的日子.记得那时候公司里有些开发和测试的女孩子,经常有问题解决不了的,不管什么领域的问题找到我,我都能帮她们解决.但是那时候我没有主动学习技术的意识,只 ...

随机推荐

  1. js实现网页端复制功能

    实现网页端复制功能: <div id="copyInput" style="display:none;"> <form> <inp ...

  2. struts2上传图片超过大小给出错误提示

    struts2上传图片超过大小给出错误提示        今天碰到上传图片太大,上传不上去返回input视图的界面,回显的错误信息却是乱码,整了好久才整出来,在这里做个记录,方便自己以后查阅,也希望能 ...

  3. ecshop购买记录(近期成交数量)显示历史记录

    默认只显示最近30天内的销售记录, 按照下面方法修改 就可以显示所有的了 打开 /includes/lib_insert.php 文件 找到下面代码,将之删除即可,一共有两处,都要删除. AND ' ...

  4. Webview页面的控件元素定位

    前言 现在有很多App都是Hybrid的,即有原生的页面又有Webview的页面,元素的可以通过uiautomatorviewer工具 进行控件元素的定位,Webview页面的则无法通过此方式定位,而 ...

  5. java_类型强转

    class Father{ public void fromFather(){ System.out.println("fromFather"); } } interface in ...

  6. iOS_高效开发之道

    iOS_高效开发之道 话不多说, 总结一下个人感觉有利于提高iOS开发效率的几个小技巧. 本文将从下面几方面介绍: Xcode经常使用快捷键 Xcode调试技巧 Objc经常使用代码片段 Xcode插 ...

  7. 使用git checkout 指定git代码库上的指定分支

    因为曾经一直是在用svn,到狼厂,大家都用Git. 哥的开发环境:IntelliJ 说说简单的操作过程吧. 1.检出Git代码库 cd到指定文件夹 git clone http://..../andr ...

  8. EasyDarwin开源平台直播架构

    Created with Raphaël 2.1.0ClientClientEasyCMSEasyCMSEasyCameraEasyCameraEasyDarwinEasyDarwin请求设备列表设备 ...

  9. Theseven relationsarein threecategories:equivalent, congruent, andsimilar.

    http://www.math.pitt.edu/~xfc/math2370/chap5.pdf

  10. 对于js里的闭包的理解

    Ali的回答: 当function里嵌套function时,内部的function可以访问外部function里的变量. function foo(x) {     var tmp = 3;      ...