同 HashMap 一样,LinkedHashMap 也是对 Map 接口的一种基于链表和哈希表的实现。实际上, LinkedHashMap 是 HashMap 的子类,其扩展了 HashMap 增加了双向链表的实现。相较于 HashMap 的迭代器中混乱的访问顺序,LinkedHashMap 可以提供可以预测的迭代访问,即按照插入序 (insertion-order) 或访问序 (access-order) 来对哈希表中的元素进行迭代。

 public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>

但是,和 HashMap 有所区别的是,LinkedHashMap 支持按插入序 (insertion-order) 或访问序 (access-order) 来访问其中的元素。所谓插入顺序,就是 Entry 被添加到 Map 中的顺序,更新一个 Key 关联的 Value 并不会对插入顺序造成影响;而访问顺序则是对所有 Entry 按照最近访问 (least-recently) 到最远访问 (most-recently) 进行排序,读写都会影响到访问顺序,但是对迭代器 (entrySet(), keySet(), values()) 的访问不会影响到访问顺序。访问序的特性使得可以很容易通过 LinkedHashMap 来实现一个 LRU(least-recently-used) Cache,后面会给出一个简单的例子。从类声明中可以看到,LinkedHashMap 确实是继承了 HashMap,因而 HashMap 中的一些基本操作,如哈希计算、扩容、查找等,在 LinkedHashMap 中都和父类 HashMap 是一致的。

之所以 LinkedHashMap 能够支持插入序或访问序的遍历,是因为 LinkedHashMap 在 HashMap 的基础上增加了双向链表的实现,下面会结合 JDK 8 的源码进行简要的分析。

底层结构

LinkedHashMap 是 HashMap 的子类,因而 HashMap 中的成员在 LinkedHashMap 中也存在,如底层的 table 数组等,这里就不再说明了。我们重点关注一下 LinkedHashMap 中节点发生的变化。

 /**
* HashMap.Node subclass for normal LinkedHashMap entries.
*/
//LinkedHashMap.Entey 继承自 HashMap.Node,
//而 HashMap.TreeNode 又继承了 LinkedHashMap.Entey
static class Entry<K,V> extends HashMap.Node<K,V> {
//在父类的基础上增加了before 和 after
//父类中存在 next
//双向链表的连接通过before 和 after,哈希表中所有的元素可看作一个双向链表
//桶内单向链表的连接通过 next
Entry<K,V> before, after;
//构造方法
Entry(int hash, K key, V value, Node<K,V> next) {
//父类构造方法
super(hash, key, value, next);
}
} private static final long serialVersionUID = 3801124242820219131L; /**
* The head (eldest) of the doubly linked list.
*/
//head成员为双向链表的头
transient LinkedHashMap.Entry<K,V> head; /**
* The tail (youngest) of the doubly linked list.
*/
//tail成员为双向链表的尾
transient LinkedHashMap.Entry<K,V> tail; /**
* The iteration ordering method for this linked hash map: <tt>true</tt>
* for access-order, <tt>false</tt> for insertion-order.
*
* @serial
*/
//迭代顺序, true 使用最近被访问的顺序, false为插入顺序
//the order in which its entries were last accessed, from least-recently accessed to most-recently (access-order), well-suited to building LRU caches
final boolean accessOrder;

由于父类 HashMap 的节点中存在 next 引用,可以将每个桶中的元素都当作一个单链表看待;LinkedHashMap 的每个桶中当然也保留了这个单链表关系,不过这个关系由父类进行管理,LinkedHashMap 中只会对双向链表的关系进行管理。LinkedHashMap 中所有的元素都被串联在一个双向链表中。为了实现双向链表,LinkedHashMap 的节点在父类的基础上增加了 before/after 引用,并且使用 head 和 tail 分别保存双向链表的头和尾。同时,增加了一个标识来保存 LinkedHashMap 的迭代顺序是插入序还是访问序。

双向链表

为了简化对双向链表的操作,LinkedHashMap 中提供了 linkNodeLast 和 transferLinks 方法,分别如下:

 // link at the end of list
// 将新节点 p 链接到双向链表的末尾
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null)
//为空,则为头节点
head = p;
else {
//修改before 和 after的指向
p.before = last;
last.after = p;
}
} // apply src's links to dst
// 将src的链接应用到dst中,就是用dst替换src在双向链表中的位置
private void transferLinks(LinkedHashMap.Entry<K,V> src,
LinkedHashMap.Entry<K,V> dst) {
//修改dst的前驱和后继指向
LinkedHashMap.Entry<K,V> b = dst.before = src.before;
LinkedHashMap.Entry<K,V> a = dst.after = src.after;
//将双向链表中原来指向src的链接改为指向dst
if (b == null)
head = dst;
else
b.after = dst;
if (a == null)
tail = dst;
else
a.before = dst;
}

LinkedHashMap 重写了父类新建节点的方法,在新建节点之后调用 linkNodeLast 方法将新添加的节点链接到双向链表的末尾:

 //覆盖父类方法
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;
} //覆盖父类方法
//新建TreeNode
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
//将新建的节点链接到双向链表的末尾
linkNodeLast(p);
return p;
}

我们知道,HashMap 中单个桶中的元素可能会在单链表和红黑树之间进行转换,LinkedHashMap 中当然也是一样,不过在转换时还要调用 transferLinks 来改变双向链表中的连接关系:

 //覆盖父类方法
// For conversion from TreeNodes to plain nodes
// 将节点由 TreeNode 转换为 普通节点
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p; //TreeNode
//根据TreeNode的信息创建新的普通节点
LinkedHashMap.Entry<K,V> t =
new LinkedHashMap.Entry<K,V>(q.hash, q.key, q.value, next);
//将双向链表中的TreeNode替换为新的普通节点
transferLinks(q, t);
return t;
} //覆盖父类方法
// For treeifyBin
// 将节点由普通节点转换为TreeNode
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
TreeNode<K,V> t = new TreeNode<K,V>(q.hash, q.key, q.value, next);
transferLinks(q, t);
return t;
}

在 LinkedHashMap 中,所有的 Entry 都被串联在一个双向链表中。从上一节的代码中可以看到,每次在新建一个节点时都会将新建的节点链接到双向链表的末尾。这样从双向链表的尾部向头部遍历就可以保证插入顺序了,头部节点是最早添加的节点,而尾部节点则是最近添加的节点。那么,访问顺序要怎么实现呢?如何维护插入序和访问序?

之前我们在分析 HashMap 的源码的时候,在添加及更新、查找、删除等操作中可以看到 afterNodeAccess、afterNodeInsertion、afterNodeRemoval 等几个方法的调用,不过在 HashMap 中这几个方法中没有任何操作。实际上,这几个方法就是供 LinkedHashMap 的重写的,我们不妨看一下在 HashMap 中这几个方法的声明:

 // Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }

在 LinkedHashMap 中对这几个方法进行了重写:

 //移除节点的回调函数
void afterNodeRemoval(Node<K,V> e) { // unlink
//移除一个节点,双向链表中的连接关系也要调整
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
} //插入节点的回调函数
//构造函数中调用,evict为false
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
//first是头元素,也是最老的元素
//在插入序中,就是最先插入的元素
//在访问序中,就是最远被访问的元素
//这里removeEldestEntry(first)始终返回true,即不删除最老的元素
//如果是一个容量固定的cache,可调整removeEldestEntry(first)的实现
if (evict && (first = head) != null && removeEldestEntry(first)) {
//不是构造方法中
//头元素不为空
//要删除最老的元素
//在LinkedHashMap的实现中,不会进入这里
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
} //访问节点的回调函数
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
//如果是访问序,且当前节点并不是尾节点
//将该节点置为双向链表的尾部
if (accessOrder && (last = tail) != e) {
//p 当前节点, b 前驱结点, a 后继结点
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null; //设为尾节点,则没有后继
if (b == null)
head = a; //p是头节点,调整后其后继结点变为头节点
else
b.after = a;//p不是头节点,前驱和后继结点相连
if (a != null)
a.before = b;
else
last = b;//应该不会出现这种情况,p是尾节点
if (last == null)
head = p;
else {
//将p置于尾节点之后
p.before = last;
last.after = p;
}
tail = p;//调整tail指向
++modCount;//结构性改变
}
}

在插入节点、删除节点和访问节点后会调用相应的回调函数。可以看到,在 afterNodeAccess 方法中,如果该 LinkedHashMap 是访问序,且当前访问的节点不是尾部节点,则该节点会被置为双链表的尾节点。即,在访问序下,最近访问的节点会是尾节点,头节点则是最远访问的节点。

在 afterNodeInsertion 中,如果 removeEldestEntry(first) 节点返回 true,则会将头部节点删除。如果想要实现一个固定容量的 Map,可以在继承 LinkedHashMap 后重写 removeEldestEntry 方法。在 LinkedHashMap 中,该方法始终返回 false。

 //返回false
//是否移除最老的Entry
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}

遍历及迭代器在 HashMap 中,在 putVal 和 removeNode 中都调用了相应的回调函数,而 get 则没有,因而在 LinkedHahsMap 中进行了重写:

 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;
}

因为 LinkeHashMap 的所有的节点都在一个双向链表中,因而可以通过该双向链表来遍历所有的 Entry。而在 HashMap 中,要遍历所有的 Entry,则要依次遍历所有桶中的单链表。相比较而言,从时间复杂度的角度来看,LinkedHashMap 的复杂度为 O(size()),而 HashMap 则为 O(capacity + size())。

 //因为所有的节点都被串联在双向链表中,迭代器在迭代时可以利用双向链表的链接关系进行
//双向链表的顺序是按照插入序或访问序排列的
//相比于HashMap中的迭代,LinkedHashMap更为高效,O(size())
//HashMapde 迭代,O(capacity + size())
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next;
LinkedHashMap.Entry<K,V> current;
int expectedModCount; LinkedHashIterator() {
next = head;
expectedModCount = modCount;
current = null;
} public final boolean hasNext() {
return next != null;
} final LinkedHashMap.Entry<K,V> nextNode() {
LinkedHashMap.Entry<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
current = e;
next = e.after;
return e;
} 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;
}
}

在 containsValue 和 internalWriteEntries 中也使用了双向链表进行遍历。可以看到,在遍历所有节点时是通过节点的 after 引用进行的。这样,可以双链表的头部遍历到到双链表的尾部,就不用像 HahsMap 那样访问空槽了。

 public boolean containsValue(Object 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;
} //覆盖父类方法
//序列化,Called only from writeObject, to ensure compatible ordering.
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
//调整元素的遍历方式,使用双链表遍历
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
s.writeObject(e.key);
s.writeObject(e.value);
}
}

LinkedHashMap 的访问序可以方便地用来实现一个 LRU Cache。在访问序模式下,尾部节点是最近一次被访问的节点 (least-recently),而头部节点则是最远访问 (most-recently) 的节点。因而在决定失效缓存的时候,将头部节点移除即可。使用 LinkedHashMap 实现 LRU Cache

但是,由于链表是无界的,但缓存往往是资源受限的,如何确定何时移除最远访问的缓存呢?前面分析过,在 afterNodeInsertion 中,会调用 removeEldestEntry 来决定是否将最老的节点移除,因而我们可以使用 LinkedHashMap 的子类,并重写 removeEldestEntry 方法,当 Enrty 的数量超过缓存的容量是返回 true 即可。

下面给出基于 LinkedHashMap 实现的 LRU Cache 的代码:

 public class CacheImpl<K,V> {
private Map<K, V> cache;
private int capacity; public enum POLICY {
LRU, FIFO
} public CacheImpl(int cap, POLICY policy) {
this.capacity = cap;
cache = new LinkedHashMap<K, V>(cap, 0.75f, policy.equals(POLICY.LRU)){
//超出容量就删除最老的值
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
};
} public V get(K key) {
if (cache.containsKey(key)) {
return cache.get(key);
}
return null;
} public void set(K key, V val) {
cache.put(key, val);
} public void printKV() {
System.out.println("key value in cache");
for (Map.Entry<K,V> entry : cache.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
} public static void main(String[] args) {
CacheImpl<Integer, String> cache = new CacheImpl(5, POLICY.LRU); cache.set(1, "first");
cache.set(2, "second");
cache.set(3, "third");
cache.set(4, "fourth");
cache.set(5, "fifth");
cache.printKV(); cache.get(1);
cache.get(2);
cache.printKV(); cache.set(6, "sixth");
cache.printKV();
}
}

小结

本文对 JDK 8 中的 LinkedHashMap 的源码及实现进行了简单的分析。LinkedHashMap 继承自 HashMap,并在其基本结构上增加了双向链表的实现,因而 LinkedHashMap 在内存占用上要比 HashMap 高出许多。LinkedHashMap 仍然沿用了 HashMap 中基于桶数组、桶内单链表和红黑树结构的哈希表,在哈希计算、定位、扩容等方面都和 HashMAp 是一致的。LinkedHashMap 同样支持为 null 的键和值。

由于增加了双向链表将所有的 Entry 串在一起,LinkedHashMap 的一个重要的特点就是支持按照插入顺序或访问顺序来遍历所有的 Entry,这一点和 HashMap 的乱序遍历很不相同。在一些对顺序有要求的场合,就需要使用 LinkedHashMap 来替代 HashMap。

由于双向链表的缘故,在遍历时可以直接在双向链表上进行,因而遍历时间复杂度和容量无关,只和当前 Entry 数量有关。这点相比于 HashMap 要更加高效一些。

Java 容器 LinkedHashMap源码分析1的更多相关文章

  1. Java 容器 LinkedHashMap源码分析2

    一.类签名 LinkedHashMap<K,V>继承自HashMap<K,V>,可知存入的节点key永远是唯一的.可以通过Android的LruCache了解LinkedHas ...

  2. Java容器 | 基于源码分析List集合体系

    一.容器之List集合 List集合体系应该是日常开发中最常用的API,而且通常是作为面试压轴问题(JVM.集合.并发),集合这块代码的整体设计也是融合很多编程思想,对于程序员来说具有很高的参考和借鉴 ...

  3. Java容器 | 基于源码分析Map集合体系

    一.容器之Map集合 集合体系的源码中,Map中的HashMap的设计堪称最经典,涉及数据结构.编程思想.哈希计算等等,在日常开发中对于一些源码的思想进行参考借鉴还是很有必要的. 基础:元素增查删.容 ...

  4. Java split方法源码分析

    Java split方法源码分析 public String[] split(CharSequence input [, int limit]) { int index = 0; // 指针 bool ...

  5. 【JAVA】ThreadLocal源码分析

    ThreadLocal内部是用一张哈希表来存储: static class ThreadLocalMap { static class Entry extends WeakReference<T ...

  6. 【Java】HashMap源码分析——常用方法详解

    上一篇介绍了HashMap的基本概念,这一篇着重介绍HasHMap中的一些常用方法:put()get()**resize()** 首先介绍resize()这个方法,在我看来这是HashMap中一个非常 ...

  7. 【Java】HashMap源码分析——基本概念

    在JDK1.8后,对HashMap源码进行了更改,引入了红黑树.在这之前,HashMap实际上就是就是数组+链表的结构,由于HashMap是一张哈希表,其会产生哈希冲突,为了解决哈希冲突,HashMa ...

  8. 细说并发5:Java 阻塞队列源码分析(下)

    上一篇 细说并发4:Java 阻塞队列源码分析(上) 我们了解了 ArrayBlockingQueue, LinkedBlockingQueue 和 PriorityBlockingQueue,这篇文 ...

  9. Java集合系列[4]----LinkedHashMap源码分析

    这篇文章我们开始分析LinkedHashMap的源码,LinkedHashMap继承了HashMap,也就是说LinkedHashMap是在HashMap的基础上扩展而来的,因此在看LinkedHas ...

随机推荐

  1. Kotlin语言学习笔记(6)

    运算符重载(Operator overloading) 一元运算符 Expression Translated to +a a.unaryPlus() -a a.unaryMinus() !a a.n ...

  2. Spring Boot logback

    前言 今天来介绍下spring Boot如何配置日志logback,我刚学习的时候,是带着下面几个问题来查资料的,你呢 如何引入日志? 日志输出格式以及输出方式如何配置? 代码中如何使用? 正文 Sp ...

  3. java线程状态及转换

    java线程有6种状态: 新建线程new,启动线程runnable,阻塞block,限时等待timed_waiting,等待线程waiting,终止线程terminated 1.限时等待timed w ...

  4. HTML5 historyState pushState、replaceState

    DOM中的window对象通过window.history方法提供了对浏览器历史记录的读取,让你可以在用户的访问记录中前进和后退. 从HTML5开始,我们可以开始操作这个历史记录堆栈. 1.Histo ...

  5. Linux 下 FastDFS v5.08 分布式文件系统的安装

    一.系统安装目录 源代码包目录 /data/wwwroot libevent安装目录 /usr/local/libevent FastDFS安装目录 /data/fastdfs nginx安装目录 / ...

  6. MVC仓储执行存储过程报错“未提供该参数”

    今天做的时候出现错误: "过程或函数 'sp_ProcName' 需要参数 '@uid',但未提供该参数. 可是我参数都传了,然后调试也是一样,然后对照参数列表, 后来发现执行的时候还要加入 ...

  7. Ubuntu部分快捷键的使用简介

    1.切换到字符界面 Ctrl+Alt+F1 切换到tty1字符界面 Ctrl+Alt+F2 切换到tty2字符界面 Ctrl+Alt+F3 切换到tty3字符界面 Ctrl+Alt+F4 切换到tty ...

  8. Netty 系列目录

    Netty 系列目录 二 Netty 源码分析(4.1.20) 1.1 Netty 源码(一)Netty 组件简介 2.1 Netty 源码(一)服务端启动 2.2 Netty 源码(二)客户端启动 ...

  9. Netty 零拷贝(一)Linux 零拷贝

    Netty 零拷贝(一)Linux 零拷贝 本文探讨 Linux 中主要的几种零拷贝技术以及零拷贝技术适用的场景. 一.几个重要的概念 1.1 用户空间与内核空间 操作系统的核心是内核,独立于普通的应 ...

  10. 调用数据库--stone

    from Mysql_operate_class import mysql def saveMysqlData(sql, dbname="algorithm"): pym = my ...