知识准备HashMap

我们平时用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原理分析的更多相关文章

  1. Java基础之HashMap原理分析(put、get、resize)

    在分析HashMap之前,先看下图,理解一下HashMap的结构 我手画了一个图,简单描述一下HashMap的结构,数组+链表构成一个HashMap,当我们调用put方法的时候增加一个新的 key-v ...

  2. 原子类java.util.concurrent.atomic.*原理分析

    原子类java.util.concurrent.atomic.*原理分析 在并发编程下,原子操作类的应用可以说是无处不在的.为解决线程安全的读写提供了很大的便利. 原子类保证原子的两个关键的点就是:可 ...

  3. JAVA常用数据结构及原理分析

    JAVA常用数据结构及原理分析 http://www.2cto.com/kf/201506/412305.html 前不久面试官让我说一下怎么理解java数据结构框架,之前也看过部分源码,balaba ...

  4. Java NIO使用及原理分析 (四)

    在上一篇文章中介绍了关于缓冲区的一些细节内容,现在终于可以进入NIO中最有意思的部分非阻塞I/O.通常在进行同步I/O操作时,如果读取数据,代码会阻塞直至有 可供读取的数据.同样,写入调用将会阻塞直至 ...

  5. (6)Java数据结构-- 转:JAVA常用数据结构及原理分析

    JAVA常用数据结构及原理分析  http://www.2cto.com/kf/201506/412305.html 前不久面试官让我说一下怎么理解java数据结构框架,之前也看过部分源码,balab ...

  6. Java NIO使用及原理分析 (四)(转)

    在上一篇文章中介绍了关于缓冲区的一些细节内容,现在终于可以进入NIO中最有意思的部分非阻塞I/O.通常在进行同步I/O操作时,如果读取数据,代码会阻塞直至有 可供读取的数据.同样,写入调用将会阻塞直至 ...

  7. 支付宝app支付java后台流程、原理分析(含nei wang chuan tou)

    java版支付宝app支付流程及原理分析 本实例是基于springmvc框架编写     一.流程步骤         1.执行流程           当手机端app(就是你公司开发的app)在支付 ...

  8. Java NIO使用及原理分析(1-4)(转)

    转载的原文章也找不到!从以下博客中找到http://blog.csdn.net/wuxianglong/article/details/6604817 转载自:李会军•宁静致远 最近由于工作关系要做一 ...

  9. Java NIO使用及原理分析(二)

    在第一篇中,我们介绍了NIO中的两个核心对象:缓冲区和通道,在谈到缓冲区时,我们说缓冲区对象本质上是一个数组,但它其实是一个特殊的数组,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况,如 ...

随机推荐

  1. 编译hotspot8

    编译hotspot8 ubuntu desktop 18 全新准备与编译过程再记录下: # 建议使用此gcc和g++版本,过高版本比如gcc7或引发编译报错 sudo apt-get install ...

  2. Typescript node starter 3. App Router Controller

    Request request对象表示HTTP请求,并具有请求query字符串.参数.body.HTTP headers等的属性.除了添加新的属性和方法外,还包含原型的属性和方法. 随着系列文章的发布 ...

  3. java应用中的日志介绍

    日志在应用程序中是非常非常重要的,好的日志信息能有助于我们在程序出现 BUG 时能快速进行定位,并能找出其中的原因. 但是,很多介绍 AOP 的地方都采用日志来作为介绍,实际上日志要采用切面的话是极其 ...

  4. Antd cracoTs Js 配置流程

    JS:文档:0.1.4 配置 js 环境.note链接:http://note.youdao.com/noteshare?id=e32fa75c1baa014b5819fa5e22887dbc& ...

  5. Spark中遇到的问题

    spark启动slave时提示 JAVA_HOME is not set 解决方法: 在sbin目录spark-config.sh 中添加自己的jdk 路径export JAVA_HOME=/home ...

  6. js中的寄生组合继承

    function inheritProperty(subType, superType) { function F(){} F.prototype = superType.prototype; sup ...

  7. html的JavaScript的简单输入验证

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  8. Java 8新特性(一):Lambda表达式

    2014年3月发布的Java 8,有可能是Java版本更新中变化最大的一次.新的Java 8为开发者带来了许多重量级的新特性,包括Lambda表达式,流式数据处理,新的Optional类,新的日期和时 ...

  9. 服务器基本配置(ubuntu)

    服务器基本配置(ubuntu) 学习目标: 修改初始服务器名字(ubuntu 16.04 ) 修改初始服务器名字(ubuntu 18.04 ) ubuntu换源 更改默认python版本 安装软件出现 ...

  10. Mac包管理神器:Home-brew

    最近看到一个大神修改的Homebrew国内脚本,安装非常方便,以前使用国外的经常下载不下来,这个感觉是非常快的. Homebrew 国内自动安装脚本 ,修改原脚本中的 clone 操作为“浅拷贝”(- ...