概述

LinkedHashMap是一个关联数组、哈希表,它是线程不安全的,允许key为null,value为null。他继承自HashMap,实现了Map<K,V>接口。其内部还维护了一个双向链表,在每次插入数据,或者访问、修改数据时,会增加节点、或调整链表的节点顺序。以决定迭代时输出的顺序。

默认情况,遍历时的顺序是按照插入节点的顺序。这也是其与HashMap最大的区别。也可以在构造时传入accessOrder参数,使得其遍历顺序按照访问的顺序输出。

构造函数

    //默认是false,则迭代时输出的顺序是插入节点的顺序。若为true,则输出的顺序是按照访问节点的顺序。
//为true时,可以在这基础之上构建一个LruCach
final boolean accessOrder; public LinkedHashMap() {
super();
accessOrder = false;
}
//指定初始化时的容量,
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
//指定初始化时的容量,和扩容的加载因子
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
//指定初始化时的容量,和扩容的加载因子,以及迭代输出节点的顺序
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
//利用另一个Map 来构建,
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
//该方法上文分析过,批量插入一个map中的所有数据到 本集合中。
putMapEntries(m, false);
}

小结

构造函数和HashMap相比,就是增加了一个accessOrder参数。用于控制迭代时的节点顺序。

节点

LinkedHashMap的节点Entry<K,V>继承自HashMap<K,V>,在其基础上扩展了一下。改成了一个双向链表

    static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}

同时类里有两个成员变量head tail,分别指向内部双向链表的表头、表尾。

    //双向链表的头结点
transient LinkedHashMap.Entry<K,V> head; //双向链表的尾节点
transient LinkedHashMap.Entry<K,V> tail;

LinkedHashMap并没有重写任何put方法。但是其重写了构建新节点的newNode()方法.

newNode()会在HashMapputVal()方法里被调用,putVal()方法会在批量插入数据putMapEntries(Map<? extends K, ? extends V> m, boolean evict)或者插入单个数据public V put(K key, V value)时被调用。

LinkedHashMap重写了newNode(),在每次构建新节点时,通过linkNodeLast(p);将新节点链接在内部双向链表的尾部

    //在构建新节点时,构建的是`LinkedHashMap.Entry` 不再是`Node`.
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
//将新增的节点,连接在链表的尾部
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
//集合之前是空的
if (last == null)
head = p;
else {//将新节点连接在链表的尾部
p.before = last;
last.after = p;
}
}

以及HashMap专门预留给LinkedHashMapafterNodeAccess() afterNodeInsertion() afterNodeRemoval()方法。

    // Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
//回调函数,新节点插入之后回调 , 根据evict 和 判断是否需要删除最老插入的节点。如果实现LruCache会用到这个方法。
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
//LinkedHashMap 默认返回false 则不删除节点
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
//LinkedHashMap 默认返回false 则不删除节点。 返回true 代表要删除最早的节点。通常构建一个LruCache会在达到Cache的上限是返回true
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}

void afterNodeInsertion(boolean evict)以及boolean removeEldestEntry(Map.Entry<K,V> eldest)是构建LruCache需要的回调,在LinkedHashMap里可以忽略它们。

LinkedHashMap也没有重写remove()方法,因为它的删除逻辑和HashMap并无区别。

但它重写了afterNodeRemoval()这个回调方法。该方法会在Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable)方法中回调,removeNode()会在所有涉及到删除节点的方法中被调用,是删除节点操作的真正执行者。

    //在删除节点e时,同步将e从双向链表上删除
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//待删除节点 p 的前置后置节点都置空
p.before = p.after = null;
//如果前置节点是null,则现在的头结点应该是后置节点a
if (b == null)
head = a;
else//否则将前置节点b的后置节点指向a
b.after = a;
//同理如果后置节点时null ,则尾节点应是b
if (a == null)
tail = b;
else//否则更新后置节点a的前置节点为b
a.before = b;
}

LinkedHashMap重写了get()getOrDefault()方法:

    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;
}
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return defaultValue;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}

对比HashMap中的实现,LinkedHashMap只是增加了在成员变量(构造函数时赋值)accessOrder为true的情况下,要去回调void afterNodeAccess(Node<K,V> e)函数。

    public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

afterNodeAccess()函数中,会将当前被访问到的节点e,移动至内部的双向链表的尾部

    void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;//原尾节点
//如果accessOrder 是true ,且原尾节点不等于e
if (accessOrder && (last = tail) != e) {
//节点e强转成双向链表节点p
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//p现在是尾节点, 后置节点一定是null
p.after = null;
//如果p的前置节点是null,则p以前是头结点,所以更新现在的头结点是p的后置节点a
if (b == null)
head = a;
else//否则更新p的前直接点b的后置节点为 a
b.after = a;
//如果p的后置节点不是null,则更新后置节点a的前置节点为b
if (a != null)
a.before = b;
else//如果原本p的后置节点是null,则p就是尾节点。 此时 更新last的引用为 p的前置节点b
last = b;
if (last == null) //原本尾节点是null 则,链表中就一个节点
head = p;
else {//否则 更新 当前节点p的前置节点为 原尾节点last, last的后置节点是p
p.before = last;
last.after = p;
}
//尾节点的引用赋值成p
tail = p;
//修改modCount。
++modCount;
}
}

值得注意的是,afterNodeAccess()函数中,会修改modCount,因此当你正在accessOrder=true的模式下,迭代LinkedHashMap时,如果同时查询访问数据,也会导致fail-fast,因为迭代的顺序已经改变。

containsValue

它重写了该方法,相比HashMap的实现,更为高效

    public boolean containsValue(Object value) {
//遍历一遍链表,去比较有没有value相等的节点,并返回
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
V v = e.value;
if (v == value || (value != null && value.equals(v)))
return true;
}
return false;
}

对比HashMap,是用两个for循环遍历,相对低效。

    public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
if ((tab = table) != null && size > 0) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
if ((v = e.value) == value ||
(value != null && value.equals(v)))
return true;
}
}
}
return false;
}

遍历

重写了entrySet()如下:

    public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
//返回LinkedEntrySet
return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}
final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
public final Iterator<Map.Entry<K,V>> iterator() {
return new LinkedEntryIterator();
}
}

最终的EntryIterator:

    final class LinkedEntryIterator extends LinkedHashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
} abstract class LinkedHashIterator {
//下一个节点
LinkedHashMap.Entry<K,V> next;
//当前节点
LinkedHashMap.Entry<K,V> current;
int expectedModCount; LinkedHashIterator() {
//初始化时,next 为 LinkedHashMap内部维护的双向链表的扁头
next = head;
//记录当前modCount,以满足fail-fast
expectedModCount = modCount;
//当前节点为null
current = null;
}
//判断是否还有next
public final boolean hasNext() {
//就是判断next是否为null,默认next是head 表头
return next != null;
}
//nextNode() 就是迭代器里的next()方法 。
//该方法的实现可以看出,迭代LinkedHashMap,就是从内部维护的双链表的表头开始循环输出。
final LinkedHashMap.Entry<K,V> nextNode() {
//记录要返回的e。
LinkedHashMap.Entry<K,V> e = next;
//判断fail-fast
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
//如果要返回的节点是null,异常
if (e == null)
throw new NoSuchElementException();
//更新当前节点为e
current = e;
//更新下一个节点是e的后置节点
next = e.after;
//返回e
return e;
}
//删除方法 最终还是调用了HashMap的removeNode方法
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}

值得注意的就是:nextNode()就是迭代器里的next()方法 。

该方法的实现可以看出,迭代LinkedHashMap,就是从内部维护的双链表的表头开始循环输出

而双链表节点的顺序在LinkedHashMap的增、删、改、查时都会更新。以满足按照插入顺序输出,还是访问顺序输出

总结

LinkedHashMap相对于HashMap的源码比,是很简单的。因为大树底下好乘凉。它继承了HashMap,仅重写了几个方法,以改变它迭代遍历时的顺序。这也是其与HashMap相比最大的不同。

在每次插入数据,或者访问、修改数据时,会增加节点、或调整链表的节点顺序。以决定迭代时输出的顺序。

  • accessOrder,默认是false,则迭代时输出的顺序是插入节点的顺序。若为true,则输出的顺序是按照访问节点的顺序。为true时,可以在这基础之上构建一个LruCache.
  • LinkedHashMap并没有重写任何put方法。但是其重写了构建新节点的newNode()方法.在每次构建新节点时,将新节点链接在内部双向链表的尾部.
  • accessOrder=true的模式下,在afterNodeAccess()函数中,会将当前被访问到的节点e,移动至内部的双向链表的尾部。值得注意的是,afterNodeAccess()函数中,会修改modCount,因此当你正在accessOrder=true的模式下,迭代LinkedHashMap时,如果同时查询访问数据,也会导致fail-fast,因为迭代的顺序已经改变.
  • nextNode()就是迭代器里的next()方法。该方法的实现可以看出,迭代LinkedHashMap,就是从内部维护的双链表的表头开始循环输出。而双链表节点的顺序在LinkedHashMap增、删、改、查时都会更新。以满足按照插入顺序输出,还是访问顺序输出.
  • 它与HashMap比,还有一个小小的优化,重写了containsValue()方法,直接遍历内部链表去比对value值是否相等.

JDK源码分析(9) LinkedHashMap的更多相关文章

  1. JDK源码分析(四)——LinkedHashMap

    目录 LinkedHashMap概述 内部字段及构造方法 存储元素 取出元素 删除元素 迭代器 利用LinkedHashMap简单实现LRU算法 总结 LinkedHashMap概述   JDK对Li ...

  2. JDK 源码分析(4)—— HashMap/LinkedHashMap/Hashtable

    JDK 源码分析(4)-- HashMap/LinkedHashMap/Hashtable HashMap HashMap采用的是哈希算法+链表冲突解决,table的大小永远为2次幂,因为在初始化的时 ...

  3. 【JDK】JDK源码分析-LinkedHashMap

    概述 前文「JDK源码分析-HashMap(1)」分析了 HashMap 主要方法的实现原理(其他问题以后分析),本文分析下 LinkedHashMap. 先看一下 LinkedHashMap 的类继 ...

  4. 【JDK】JDK源码分析-HashMap(1)

    概述 HashMap 是 Java 开发中最常用的容器类之一,也是面试的常客.它其实就是前文「数据结构与算法笔记(二)」中「散列表」的实现,处理散列冲突用的是“链表法”,并且在 JDK 1.8 做了优 ...

  5. JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue

    JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue 目的:本文通过分析JDK源码来对比ArrayBlockingQueue 和LinkedBlocki ...

  6. JDK源码分析(三)—— LinkedList

    参考文档 JDK源码分析(4)之 LinkedList 相关

  7. JDK源码分析(一)—— String

    dir 参考文档 JDK源码分析(1)之 String 相关

  8. JDK源码分析(2)LinkedList

    JDK版本 LinkedList简介 LinkedList 是一个继承于AbstractSequentialList的双向链表.它也可以被当作堆栈.队列或双端队列进行操作. LinkedList 实现 ...

  9. 【JDK】JDK源码分析-TreeMap(2)

    前文「JDK源码分析-TreeMap(1)」分析了 TreeMap 的一些方法,本文分析其中的增删方法.这也是红黑树插入和删除节点的操作,由于相对复杂,因此单独进行分析. 插入操作 该操作其实就是红黑 ...

  10. 【JDK】JDK源码分析-Vector

    概述 上文「JDK源码分析-ArrayList」主要分析了 ArrayList 的实现原理.本文分析 List 接口的另一个实现类:Vector. Vector 的内部实现与 ArrayList 类似 ...

随机推荐

  1. CODING 如何使用 CODING 研发管理系统来敏捷开发

    之前我们分享过<CODING 如何使用 CODING 开发 CODING>的文章,时过境迁,现在 CODING 研发管理系统已经上线了如持续集成.缺陷管理.测试管理等 DevOps 中的重 ...

  2. harris角点检测的简要总结

    目录 1. 概述相关 2. 原理详解 1) 算法思想 2) 数学模型 3) 优化推导 3. 具体实现 1) 详细步骤 2) 最终实现 4. 参考文献 1. 概述相关 harris角点检测是一种特征提取 ...

  3. 处理范例代码Webapi中的Mongodb的Bson中ObjectId反序列化异常

    微软代码范例中的一个Bug 处理Mongodb的Bson中ObjectId反序列化异常 https://docs.microsoft.com/zh-cn/aspnet/core/tutorials/f ...

  4. KsUML 免费的类图建模工具

    最近基于SharpDevelop和NClass两个开源软件,开发了一个免费的类图建模工具,详情请访问 www.TimeGIS.com KsUML类图建模工具是一个用来给软件开发人员使用的一种UML类图 ...

  5. 基于GitLab的Code Review教程

    一.前言 1.本文主要内容 GitLab Code Review机制说明 Git Workflow 与 Git Code Review Workflow GitLab Code Review 配置说明 ...

  6. WebApi 身份认证解决方案:Basic基础认证

    前言:最近,讨论到数据库安全的问题,于是就引出了WebApi服务没有加任何验证的问题.也就是说,任何人只要知道了接口的url,都能够模拟http请求去访问我们的服务接口,从而去增删改查数据库,这后果想 ...

  7. eclipse导入maven项目,但无法编译的问题

    同事今天从git 导入项目到eclipse 后,发现项目所依赖的包找不到依赖,初步判定是maven的依赖没有导入项目中. 最终发现,在项目中的.classpath 文件加入以下代码即可解决问题. &l ...

  8. ​Installing the Ranger Kafka Plug-in

    This section describes how to install and enable the Ranger Kafka plug-in. The Ranger Kafka plug-in ...

  9. java学习入门之---使用idea创建第一个maven项目

    一.准备条件: 1.安装idea旗舰版 2.安装tomcat 二.打开idea开始创建 1.创建Project 2.选择项目类型为maven 3.输入组名和项目名 ---> 下一步 ----&g ...

  10. oracle 基础查询语句

    select abs(10) from dual; --取绝对值select ceil(3.6) from dual;--向上取整 select power(2,3) from dual;--2的3次 ...