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的优化把矩形核心的腐蚀和膨胀做到了不仅和半径无关,而且速度也相当的 ...
随机推荐
- 5月第2周业务风控关注 | 央行:严禁未经授权认可的APP接入征信系统
本文由 网易云发布. 易盾业务风控周报每周呈报值得关注的安全技术和事件,包括但不限于内容安全.移动安全.业务安全和网络安全,帮助企业提高警惕,规避这些似小实大.影响业务健康发展的安全风险. 1.央行 ...
- MicroService 微服务架构模式简述
开源地址: https://github.com/TheCodeCleaner/MicroService4Net 本文内容 微服务 微服务风格的特性 组件化(Componentization )与服务 ...
- 原生javascript写自己的运动库(匀速运动篇)
网上有很多JavaScript的运动库,这里和大家分享一下用原生JavaScript一步一步写一个运动函数的过程,如读者有更好的建议欢迎联系作者帮助优化完善代码.这个运动函数完成后,就可以用这个运动函 ...
- Java 使用BigDecimal类处理高精度计算
Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算.双精度浮点型变量double可以处理16位有效数,但在实际应用中,可能需要对更大或者更小的 ...
- 一个能够在Asp.Net和Asp.NetCore之间能够互相通讯的Rpc
一.特性 1.跨平台 2.提供负载均衡算法 3.支持ZK服务协调 4.提供了JSON.BinarySerializer.还有自定义的序列化方式 5.客户端提供Socket连接池,以便于快速交互,防止类 ...
- 架构选型之Nodejs与Java
前言: 身边越来越多的同事谈论Nodejs,谈其异步IO.事件回调.前后台统一一门语言,创业的朋友的第一个创业项目也选择了Nodejs,期望能够使用一种语言节省成本快速完成需求开发.与其他项目组的同事 ...
- OAuth 2.0 认证的原理与实践
摘要: 使用 OAuth 2.0 认证的的好处是显然易见的.你只需要用同一个账号密码,就能在各个网站进行访问,而免去了在每个网站都进行注册的繁琐过程. 本文将介绍 OAuth 2.0 的原理,并基于 ...
- react,react native,webpack,ES6,node.js----------今天上午学了一下node.js
http://www.yiibai.com/nodejs/node_install.html---node.js具体入门资料在此 Node JS事件循环 Node JS是单线程应用程序,但它通过事件和 ...
- css3 绘制图形
星形: .star-six { width:; height:; border-left: 50px solid transparent; border-right: 50px solid trans ...
- SQL Server 2000安装教程图解
SQL Server 2000安装教程图解... ============= 下面网盘链接中的SQL2000数据库在Win7和Win10系统上安装都是可以正常使用的,只不过是Win10上安装的话,需要 ...