Android版数据结构与算法(五):LinkedHashMap核心源码彻底分析
版权声明:本文出自汪磊的博客,未经作者允许禁止转载。
上一篇基于哈希表实现HashMap核心源码彻底分析 分析了HashMap的源码,主要分析了扩容机制,如果感兴趣的可以去看看,扩容机制那几行最难懂的代码真是花费了我很大的精力。
好了本篇我们分析一下HashMap的儿子LinkedHashMap的核心源码,提到LinkedHashMap做安卓的同学肯定会想到Lru(Least Recently Used)算法,Lru算法就是基于LinkedHashMap来实现的,明白了LinkedHashMap中基于访问排序逻辑Lru算法自然就明白了。
进入正题,源码基于android-23。
一、LinkedHashMap中成员变量
/**
* A dummy entry in the circular linked list of entries in the map.
* The first real entry is header.nxt, and the last is header.prv.
* If the map is empty, header.nxt == header && header.prv == header.
*/
transient LinkedEntry<K, V> header; /**
* True if access ordered, false if insertion ordered.
*/
private final boolean accessOrder;
很简单,就两个成员变量。不过这里要明白LinkedHashMap是继承HashMap也就是HashMap中一些成员变量,方法LinkedHashMap中都是有的,父类的玩意就不提了,这里只说一下子类自己的。
header双向循环链表的头结点,看注释The first real entry is header.nxt, and the last is header.prv.翻译过来就是第一个加入链表的结点是header.nxt,最后被加入链表的是header.prv。
accessOrder控制链表的排序方式,如果是true那么链表节点是基于访问排序的,什么是访问排序?就是我们访问链表中某一节点的时候会将这个结点从链表中删除然后在放入链表的尾部,表示用户最近使用了这个结点,最近被“宠幸”了一次,那好,我把你放入链表尾部,链表删除是从头部删除的,插入数据是从尾部插入的,如果遇到一些情况要删除链表中节点数据,那么优先删除的是链表头部不经常使用的节点数据。如果为false则表示链表节点是基于插入排序的,理解起来很简单,就是平常的插入顺序了,先插入的在头部优先被删除。
二、LinkedHashMap构造方法
接下来看下构造方法,如下:
public LinkedHashMap() {
init();
accessOrder = false;
}
public LinkedHashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public LinkedHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, false);
}
public LinkedHashMap(
int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
init();
this.accessOrder = accessOrder;
}
@Override
void init() {
header = new LinkedEntry<K, V>();
}
以上就是初始化的主要方法,大体上和HashMap差不多,不在细说,主要一点是默认的accessOrder值为false,也就是链表节点按照插入排序来排序的,当然我们也可以在初始化的时候指定accessOrder值,比如LruCache中LinkedHashMap初始化的时候accessOrder就指定为true。
三、LinkedHashMap中数据项LinkedEntry
LinkedHashMap中每个数据节点类型为LinkedEntry,LinkedEntry为HashMapEntry子类,我们直接看其源码:
static class LinkedEntry<K, V> extends HashMapEntry<K, V> {
LinkedEntry<K, V> nxt;
LinkedEntry<K, V> prv;
/** Create the header entry */
LinkedEntry() {
super(null, null, 0, null);
nxt = prv = this;
}
/** Create a normal entry */
LinkedEntry(K key, V value, int hash, HashMapEntry<K, V> next,
LinkedEntry<K, V> nxt, LinkedEntry<K, V> prv) {
super(key, value, hash, next);
this.nxt = nxt;
this.prv = prv;
}
}
LinkedEntry多了两个成员变量nxt与prv,分别只向后一个节点与前一个节点,这里暂且称呼为前向指针与后向指针,方便理解。
每个数据节点结构类似如下图所示:

并且稍有经验就知道了LinkedHashMap中链表为双向循环链表,其数据结构如下图所示:

四、LinkedHashMap中put方法
我们会发现LinkedHashMap中并没有重写put方法,只是重写了addNewEntry方法,很好理解,HashMap与LinkedHashMap二者数据结构都不一样,肯定无法共用同一个put方法,这里LinkedHashMap重写了addNewEntry方法根据自己需要放入数据即可,至于hash值,index等父类已经帮我算好了,直接继承传递过来用就可以了,接下来我们分析LinkedHashMap中addNewEntry方法,源码如下:
@Override
void addNewEntry(K key, V value, int hash, int index) {
LinkedEntry<K, V> header = this.header; // Remove eldest entry if instructed to do so.
LinkedEntry<K, V> eldest = header.nxt;
if (eldest != header && removeEldestEntry(eldest)) {
remove(eldest.key);
} // Create new entry, link it on to list, and put it into table
LinkedEntry<K, V> oldTail = header.prv;
LinkedEntry<K, V> newTail = new LinkedEntry<K,V>(
key, value, hash, table[index], header, oldTail);
table[index] = oldTail.nxt = header.prv = newTail;
}
3行,获取链表的头结点header。
6行,获取链表中最先被加入的数据节点eldest,也就是最老的数据节点,位于队头。
7-9行,判断最老的数据节点eldest与header是否相等以及removeEldestEntry(eldest)方法是否返回true,如果二者均为true则删除最老的数据节点。
什么情况下eldest与header是否相等?很简单就是链表刚刚建立的时候啊,只有一个header节点,nxt与prv指针均指向自己。
removeEldestEntry(eldest)方法源码如下:
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return false;
}
看到了吧,超级简单,直接返回false,也就是默认是不会删除最老的节点的。
12行,获取oldTail,这里oldTail是链表中最后被插入的数据节点,也就是最新的数据,位于链表最尾部。
13行,创建一个新的数据节点newTail,看这名字就知道了位于链表尾部,翻译过来就是:新的尾巴。
新节点的nxt指针指向header,prv指针指向oldTail,也就是加入之前链表的尾部,13,14行执行完链表图示如下:红色部分为即将加入链表的节点。

15行,oldTail的nxt指针指向newTail,链表的头结点header的prv指针指向newTail节点,此时链表结构如图所示:

此时,新的数据节点(红色部分)就已经插入链表了,最新插入的数据位于链表尾部。
以上就是LinkedHashMap放入数据的核心逻辑,其实很简单,就是操作双向链表而已。
接下来,我们分析get方法,看看怎么实现访问排序的。
五、LinkedHashMap中get方法
再讲get方法之前我们稍微回顾一下addNewEntry方法的13-15行,这几行中有个table[index]没有提到,其实上面我只是将双向循环链表提取出来讲放入数据的逻辑,这样理解起来比较简单,而LinkedHashMap中隐藏了HashMap中的单向链表,全部展示出其数据结构如图所示:

是不是看着乱了很多,如果一开始我就抛出此图,估计很多就蒙圈了,除去红色的线其就是一个双向循环链表,为什么这时候要抛出这个图呢?大家想一下我们如果要get一个数据没有单向链表的话很自然从header节点开始挨个遍历整个链表就完了,和LinkedList算法就很像了,显然效率低下,这里有单向链表,我们只需要算出将要获取的数据在table数组的哪一行,只需要遍历那一行单向链表就完了,效率自然提升很多,这也是LinkedHashMap中存在单向链表的意义所在。
接下来我们分析get源码:
@Override
public V get(Object key) {
/*
* This method is overridden to eliminate the need for a polymorphic
* invocation in superclass at the expense of code duplication.
*/
if (key == null) {
HashMapEntry<K, V> e = entryForNullKey;
if (e == null)
return null;
if (accessOrder)
makeTail((LinkedEntry<K, V>) e);
return e.value;
} int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
e != null; e = e.next) {
K eKey = e.key;
if (eKey == key || (e.hash == hash && key.equals(eKey))) {
if (accessOrder)
makeTail((LinkedEntry<K, V>) e);
return e.value;
}
}
return null;
}
7-14行我们不做详细分析,就是获取key的null的情况,比较简单,自己看看就行了。
16行,计算key的二次hash值,上一篇分析HashMap的时候已经分析过,不在分析。
17行,获取table数组。
18-26行,就是遍历key所在行的单向链表,21行如果链表中有此数据则执行24行逻辑返回对应数据,如果循环整个所在行单向链表都没有那么执行27行逻辑返回null,表明链表中没有我们要获取的数据。
22-23行,核心所在,我们说LinkedHashMap可以控制数据是插入排序还是访问排序,这里get方法显示就是对数据的访问,如果我们设accessOrder为true,表明我们想让LinkedHashMap数据基于访问排序,则执行makeTail方法。
接下来我们看下makeTail都做了什么。
六、LinkedHashMap中访问排序的实现
直接分析makeTail方法源码:
private void makeTail(LinkedEntry<K, V> e) {
// Unlink e
e.prv.nxt = e.nxt;
e.nxt.prv = e.prv;
// Relink e as tail
LinkedEntry<K, V> header = this.header;
LinkedEntry<K, V> oldTail = header.prv;
e.nxt = header;
e.prv = oldTail;
oldTail.nxt = header.prv = e;
modCount++;
}
又是对链表的操作,而且还是双向链表,很多同学估计一看就发愁了,静下心来,其实没那么难。
假设原链表如图所示:

此时访问数据①,我们看下makeTail是如何处理数据①的。
3行,将e所在节点的prv指针指向的节点的nxt指针指向e所在节点的nxt指针指向的节点,真是拗口。
4行,同理。
其实3,4行逻辑就是将e所在节点从链表中断开,执行完3,4行逻辑,图示如下:

主要信息图中已经体现,不在过多解释。
7,8行分别获取header与oldTail节点。
9行,将e所在节点的nxt指针指向header节点。
10行,将e所在节点的prv指针指向oldTail节点。
9,10行执行完,数据结构图示如下:

11行,oldTail所在节点的nxt指针指向e,header所在节点的prv指针指向e,11行完图示如下:

这样e所在节点就插入了链表的尾部,成为最新的数据。
makeTail方法就是将我们访问的数据通过调整指针的指向来将访问的节点调整到队列的尾部,成为最新的数据。是不是很简单?
七、总结
到此我想讲的就都完了,本篇希望你掌握LinkedHashMap的数据结构,记住有个单向链表啊,不仅仅是双向链表,否则get方法的逻辑你是看不懂的。
此外,掌握访问排序到底怎么实现的,其实很简单,就是对双向链表的操作。
好了,本篇到此结束,希望对你有用,真好!!!!!!,咱们下篇见。
Android版数据结构与算法(五):LinkedHashMap核心源码彻底分析的更多相关文章
- 6 手写Java LinkedHashMap 核心源码
概述 LinkedHashMap是Java中常用的数据结构之一,安卓中的LruCache缓存,底层使用的就是LinkedHashMap,LRU(Least Recently Used)算法,即最近最少 ...
- Android版数据结构与算法(四):基于哈希表实现HashMap核心源码彻底分析
版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 存储键值对我们首先想到HashMap,它的底层基于哈希表,采用数组存储数据,使用链表来解决哈希碰撞,它是线程不安全的,并且存储的key只能有一个为 ...
- Android版数据结构与算法(七):赫夫曼树
版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 近期忙着新版本的开发,此外正在回顾C语言,大部分时间没放在数据结构与算法的整理上,所以更新有点慢了,不过既然写了就肯定尽力将这部分完全整理好分享出 ...
- Android版数据结构与算法(一):基础简介
版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 一.前言 项目进入收尾阶段,忙忙碌碌将近一个多月吧,还好,不算太难,就是麻烦点. 数据结构与算法这个系列早就想写了,一是梳理总结,顺便逼迫自己把一 ...
- Android版数据结构与算法(三):基于链表的实现LinkedList源码彻底分析
版权声明:本文出自汪磊的博客,未经作者允许禁止转载. LinkedList 是一个双向链表.它可以被当作堆栈.队列或双端队列进行操作.LinkedList相对于ArrayList来说,添加,删除元素效 ...
- Android版数据结构与算法(二):基于数组的实现ArrayList源码彻底分析
版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 本片我们分析基础数组的实现--ArrayList,不会分析整个集合的继承体系,这不是本系列文章重点. 源码分析都是基于"安卓版" ...
- Android版数据结构与算法(六):树与二叉树
版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 之前的篇章主要讲解了数据结构中的线性结构,所谓线性结构就是数据与数据之间是一对一的关系,接下来我们就要进入非线性结构的世界了,主要是树与图,好了接 ...
- Android版数据结构与算法(八):二叉排序树
本文目录 前两篇文章我们学习了一些树的基本概念以及常用操作,本篇我们了解一下二叉树的一种特殊形式:二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree) ...
- 超越halcon速度的二值图像的腐蚀和膨胀,实现目前最快的半径相关类算法(附核心源码)。
我在两年前的博客里曾经写过 SSE图像算法优化系列七:基于SSE实现的极速的矩形核腐蚀和膨胀(最大值和最小值)算法 一文,通过SSE的优化把矩形核心的腐蚀和膨胀做到了不仅和半径无关,而且速度也相当的 ...
随机推荐
- Hello Django
首先安装Django: 1.cmd界面,输入"pip3 install django" 2.输入"django-admin help",如下图表示安装成功 ...
- linux内核中断之看门狗
一:内核中断 linux内核中的看门狗中断跟之前的裸板的中断差不多,在编写驱动之前,需要线把内核自带的watch dog模块裁剪掉,要不然会出现错误:在Device Drivers /Watchdog ...
- 数组、ArrayList、List、LinkedList的区别
一.数组 数组在内存中是连续存储的,所以它的索引速度非常快,而且赋值与修改元素也很简单. 1.一维数组 声明一个数组: ]; 初始化一个数组: ] { , , , , }; //定长 声明并初始化: ...
- Java中创建线程的三种方式及其优缺点
1.自定义一个继承Thread的类,由于Java的单继承特性,限制了该类的扩展性. 2.实现Runnable接口,重写run()方法. 3.实现Callable接口,重写call方法.线程执行体可以有 ...
- SSM-MyBatis-13:Mybatis中多条件查询
------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 实体类 public class Book { private Integer bookID; private ...
- nake_api_protect 请求保护器——防止请求被恶意刷
github : https://github.com/xjnotxj/wechat_interaction_auth -- nake_api_protect 接口请求保护器,根据 频率 + 次数 的 ...
- 根据http协议下载文件保存到相应的文件下
本实例通过提供的http网址来下载文件,并保存到本地指定的文件下. 本例提供的网址为:http://112.53.80.131:8888/database/11.mdb,下载的文件名为:11.mdb ...
- bzoj 2820 莫比乌斯反演
搞了一整个晚自习,只是看懂了dalao们的博客,目前感觉没有思路-.还是要多切题 next day: 刚才又推了一遍,发现顺过来了,hahaha #include<cstdio> #inc ...
- bzoj 1064 假面舞会 图论??+dfs
有两种情况需要考虑 1.链:可以发现对最终的k没有影响 2.环:如果是真环(即1->2->3->4->1),可以看出所有可行解一定是该环的因数 假环呢??(1->2-&g ...
- .Net Remoting 调用远程对象
根据需求,我们的系统必须以C/S方式构建,而且是三层架构,这样一来,就出现了服务器端和客户端通信的问题. 为了解决双方的通信问题,还要考虑效率.性能等方面,经过分析.试验,我们根据效率.移植.开发难易 ...