Java基础之LinkedHashMap原理分析
我们平时用LinkedHashMap的时候,都会写下面这段
LinkedHashMap<String, Object> map = new LinkedHashMap<>();
map.put("student", "333");
map.put("goods", "222");
map.put("product", "222");
然后我们通常都会去看 put 方法,但是我们点到LinkedHashMap内部后,发现没有put方法,这是为什么呢?
其实这个不难,因为LinkedHashMap继承子HashMap
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
}
这就好理解了。因为put是集成自HashMap,那么LinkedHashMap的数据也是 数组+链表 的形式存储的吗?我们慢慢往下看
在HashMap中,put一个数据的时候,会调用一个newNode方法来创建节点,而LinkedHashMap重写了该方法
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMapEntry<K,V> p =
new LinkedHashMapEntry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
在每次创建节点的时候,都调用了一次linkNodeLast方法,来拼接链表。
tail代表链表尾巴,head代表链表脑袋
entry.before代表前驱
entry.after代表后置
private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
LinkedHashMapEntry<K,V> last = tail;
tail = p;
//判断尾部是否是空的,为空就认为链表没创建,拼接在头上
if (last == null)
head = p;
else {
//在最后一个节点的before上放前一个节点
p.before = last;
//在after上放置当前节点
last.after = p;
}
}
通过这个方法,我们就对LinkedHashMap有了一个初步的了解了。before和after分别指向前驱和后置,这是典型的双向链表的结构,稍等,我去画个赋予灵魂的配图。

有了这个图就好理解多了~
当我们 put 数据的时候,除了创建节点之外,还有一个操作,就是HashMap会回调一个 afterNodeInsertion方法,我们看一下LinkedHashMap的实现
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMapEntry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
其实这个方法是移出最老的节点,但这段代码在jdk1.8里就不在被执行了,除非你自己集成LinkedHashMap重写removeEldestEntry方法。因为removeEldestEntry=false,OK,当我们在put数据的时候,整个双链表就建立起来了,接下来我们看下get有什么操作吧
final boolean accessOrder;
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
//顺序访问模式
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
LinkedHashMap的get操作首先会从 HashMap 维护的数据中通过hash获取Node,然后判断accessOrder属性,如果等于true就调用afterNodeAccess方法
那么accessOrder是个什么呢?有什么用呢?
其实accessOrder是个标记位,用来标记数据是否按照访问顺序处理,如果设置为true,那么我们每次访问数据,这个数据都会被移动到链表尾部,就会导致链表尾部的访问频次是最高的(年老的变量),链表头部是访问频次最低的(年轻的变量),这个特性正好适合做LRU缓存。如果设置为false,也就是默认的模式,那么就是按照存储顺序存储数据,访问也不会触发置尾操作。我们接下来看一下它是怎么做到的置尾吧。
首先通过这个构造方法,把accessOrder初始化成true,默认是false
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
然后我们试一下效果
Map<String, String> map = new LinkedHashMap<>(
1 << 4, 0.75f, true
);
map.put("node1", "node1");
map.put("node2", "node1");
map.put("node3", "node1");
map.put("node4", "node1");
map.get("node1");
System.out.println(map);
{node2=node1, node3=node1, node4=node1, node1=node1}
和预期一样,访问了一次node1,它就把node1放在链表的尾巴上了,这个操作主要是在afterNodeAccess内,我们接下来看下是怎么实现的吧
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
//如果我们get的数据是表头的数据,那么表头就需要更新为表头的后置
//比如node1->node2->node3,我们获取node1的时候,node1要跑到队尾
//所以node2就是老大
if (b == null)
head = a;
else
//否则的话,把get的数据的前置节点和get的数据的后置节点连接
//比如,get node2,node2的before正是node1
//因为node2要去队尾了,所以node1就不能在绑定after为node2了,要改成node3
b.after = a;
//a等于空说明p是队尾。因为只有队尾的后置节点是空的
if (a != null)
//把操作数据的后置节点连接上操作数据的前置节点
//比如,get node2,node的after便是node3
//node3的before在没改变的时候是node2,结果node2要去队尾,所以要连接都node1去
a.before = b;
else
//a等于空说明什么?说明p的后置节点是空的。说明p可能是队尾
last = b;
//假设last等于b的时候。结果b是空的,按照规则,before为空就要成为头
if (last == null)
head = p;
else {
//把操作数据的前置节点设置成队尾,准备去队尾了。。。
p.before = last;
//把刚才队尾的后置节点,设置成刚刚操作的node2,实锤了,真的都队尾了
last.after = p;
}
//执行队尾赋值
tail = p;
++modCount;
}
}
这个方法我在啰嗦总结一下吧
1.先把操作数据的前置和后置找处理
2.然后把它前置和它后置做链接
3.把它的前置链接到之前的队尾上,再把之前的队尾的后置链接到它身上
4.最后把队尾改成操作的数据即可
最后再让我这个灵魂画手配张图吧~

最后聊一下resize吧。既然是集成自HashMap,那么肯定也是到达了扩容阀值就要扩容的
我们去找LinkedhashMap内部,发现没有重写resize,那就说明它的扩容是由父类HashMap完成的。具体的扩容过程,可以看我另一篇讲解HashMap的文章
Java基础之LinkedHashMap原理分析的更多相关文章
- Java基础之HashMap原理分析(put、get、resize)
在分析HashMap之前,先看下图,理解一下HashMap的结构 我手画了一个图,简单描述一下HashMap的结构,数组+链表构成一个HashMap,当我们调用put方法的时候增加一个新的 key-v ...
- 原子类java.util.concurrent.atomic.*原理分析
原子类java.util.concurrent.atomic.*原理分析 在并发编程下,原子操作类的应用可以说是无处不在的.为解决线程安全的读写提供了很大的便利. 原子类保证原子的两个关键的点就是:可 ...
- JAVA常用数据结构及原理分析
JAVA常用数据结构及原理分析 http://www.2cto.com/kf/201506/412305.html 前不久面试官让我说一下怎么理解java数据结构框架,之前也看过部分源码,balaba ...
- Java NIO使用及原理分析 (四)
在上一篇文章中介绍了关于缓冲区的一些细节内容,现在终于可以进入NIO中最有意思的部分非阻塞I/O.通常在进行同步I/O操作时,如果读取数据,代码会阻塞直至有 可供读取的数据.同样,写入调用将会阻塞直至 ...
- (6)Java数据结构-- 转:JAVA常用数据结构及原理分析
JAVA常用数据结构及原理分析 http://www.2cto.com/kf/201506/412305.html 前不久面试官让我说一下怎么理解java数据结构框架,之前也看过部分源码,balab ...
- Java NIO使用及原理分析 (四)(转)
在上一篇文章中介绍了关于缓冲区的一些细节内容,现在终于可以进入NIO中最有意思的部分非阻塞I/O.通常在进行同步I/O操作时,如果读取数据,代码会阻塞直至有 可供读取的数据.同样,写入调用将会阻塞直至 ...
- 支付宝app支付java后台流程、原理分析(含nei wang chuan tou)
java版支付宝app支付流程及原理分析 本实例是基于springmvc框架编写 一.流程步骤 1.执行流程 当手机端app(就是你公司开发的app)在支付 ...
- Java NIO使用及原理分析(1-4)(转)
转载的原文章也找不到!从以下博客中找到http://blog.csdn.net/wuxianglong/article/details/6604817 转载自:李会军•宁静致远 最近由于工作关系要做一 ...
- Java NIO使用及原理分析(二)
在第一篇中,我们介绍了NIO中的两个核心对象:缓冲区和通道,在谈到缓冲区时,我们说缓冲区对象本质上是一个数组,但它其实是一个特殊的数组,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况,如 ...
随机推荐
- 编译hotspot8
编译hotspot8 ubuntu desktop 18 全新准备与编译过程再记录下: # 建议使用此gcc和g++版本,过高版本比如gcc7或引发编译报错 sudo apt-get install ...
- Typescript node starter 3. App Router Controller
Request request对象表示HTTP请求,并具有请求query字符串.参数.body.HTTP headers等的属性.除了添加新的属性和方法外,还包含原型的属性和方法. 随着系列文章的发布 ...
- java应用中的日志介绍
日志在应用程序中是非常非常重要的,好的日志信息能有助于我们在程序出现 BUG 时能快速进行定位,并能找出其中的原因. 但是,很多介绍 AOP 的地方都采用日志来作为介绍,实际上日志要采用切面的话是极其 ...
- Antd cracoTs Js 配置流程
JS:文档:0.1.4 配置 js 环境.note链接:http://note.youdao.com/noteshare?id=e32fa75c1baa014b5819fa5e22887dbc& ...
- Spark中遇到的问题
spark启动slave时提示 JAVA_HOME is not set 解决方法: 在sbin目录spark-config.sh 中添加自己的jdk 路径export JAVA_HOME=/home ...
- js中的寄生组合继承
function inheritProperty(subType, superType) { function F(){} F.prototype = superType.prototype; sup ...
- html的JavaScript的简单输入验证
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- Java 8新特性(一):Lambda表达式
2014年3月发布的Java 8,有可能是Java版本更新中变化最大的一次.新的Java 8为开发者带来了许多重量级的新特性,包括Lambda表达式,流式数据处理,新的Optional类,新的日期和时 ...
- 服务器基本配置(ubuntu)
服务器基本配置(ubuntu) 学习目标: 修改初始服务器名字(ubuntu 16.04 ) 修改初始服务器名字(ubuntu 18.04 ) ubuntu换源 更改默认python版本 安装软件出现 ...
- Mac包管理神器:Home-brew
最近看到一个大神修改的Homebrew国内脚本,安装非常方便,以前使用国外的经常下载不下来,这个感觉是非常快的. Homebrew 国内自动安装脚本 ,修改原脚本中的 clone 操作为“浅拷贝”(- ...