概述

LinkedHashMap是HashMap的子类,它的大部分实现与HashMap相同,两者最大的区别在于,HashMap的对哈希表进行迭代时是无序的,而LinkedHashMap对哈希表迭代是有序的,LinkedHashMap默认的规则是,迭代输出的结果保持和插入key-value pair的顺序一致(当然具体迭代规则可以修改)。LinkedHashMap除了像HashMap一样用数组、单链表和红黑树来组织数据外,还额外维护了一个双向链表,每次向linkedHashMap插入键值对,除了将其插入到哈希表的对应位置之外,还要将其插入到双向循环链表的尾部。

底层实现

先来看一下LinekedHashMap的定义:

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

除了继承自HashMap以外并无太多特殊之处,这里特地标注实现了Map接口应该也只是为了醒目。

大家最关心的应该是LinkedHashMap如何实现有序迭代,下面将逐步通过源码来解答这一问题。

先看一下一个重要的静态内部类Entry:

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

该类继承自HashMap的Node内部类,前面已经介绍过,Node是一个单链表结构,这里Entry添加了前继引用和后继引用,则是一个双向链表的节点。在双向链表中,每个节点可以记录自己前后插入的节点信息,以维持有序性,这也是LinkedHashMap实现有序迭代的关键。

按插入顺序有序和按访问顺序有序

按插入有序

按插入有序即先添加的在前面,后添加的在后面,修改操作不影响顺序。以如下代码为例:

Map<String,Integer> seqMap = new LinkedHashMap<>();

seqMap.put("c", 100);
seqMap.put("d", 200);
seqMap.put("a", 500);
seqMap.put("d", 300); for(Entry<String,Integer> entry : seqMap.entrySet()){
System.out.println(entry.getKey()+" "+entry.getValue());
}

运行结

运行结果是:

c 100
d 300
a 500

可以看到,键是按照”c”, “d”, “a”的顺序插入的,修改”d”的值不会修改顺序。

按访问有序

按访问有序是,序列末尾存放的是最近访问的key-value pair,每次访问一个key-value pair后,就会将其移动到末尾。

Map<String,Integer> accessMap = new LinkedHashMap<>(16, 0.75f, true);

accessMap.put("c", 100);
accessMap.put("d", 200);
accessMap.put("a", 500);
accessMap.get("c");
accessMap.put("d", 300); for(Entry<String,Integer> entry : accessMap.entrySet()){
System.out.println(entry.getKey()+" "+entry.getValue());
}

运行结果为:

a 500
c 100
d 300

针对不同的应用场景,LinkedHashMap可以在这两种排序方式中进行抉择。


LinkedHashMap定义了三个重要的字段:

//双链表的头节点
transient LinkedHashMap.Entry<K,V> head;
//双链表的尾节点
transient LinkedHashMap.Entry<K,V> tail;
/** * 这个字段表示哈希表的迭代顺序 * true表示按访问顺序迭代 * false表示按插入顺序迭代 * LinkedHashMap的构造函数均将该值设为false,因此默认为false */
final boolean accessOrder;

关于它们的具体作用已在注释中标出。

LinkedHashMap有五个构造方法,其中有一个可以指定accessOrder的值:

public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}

重要方法

在HashMap中定义了几个“钩子”方法(关于钩子的详细内容,请参考笔者的博客设计模式(9)——模板方法模式),这里特地列出其中的三个:

  • afterNodeRemoval(e)
  • afterNodeInsertion
  • afterNodeInsertion

它们与迭代有序性的实现息息相关。

此外还有两个重要的APIgetcontainsValue,这里也分析一下它们的源码实现,至于put方法,LinkedHashMap并没有覆写该方法,因此其实现与HashMap相同。

afterNodeRemoval(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.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}

在HashMap的removeNode方法中调用了该钩子方法,对于LinkedHashMap,在执行完对哈希桶中单链表或红黑树节点的删除操作后,还需要调用该方法将双向链表中对应的Entry删除。

afterNodeInsertion方法

void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)){
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}

在HashMap的putVal方法中调用了该方法,可以看出,在判断条件成立的情况下,该方法会删除双链表中的头节点(当然是在哈希桶和双向链表中同步删除该节点)。判断条件涉及了一个removeEldestEntry(first)方法,它的源码如下:

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}

可以看到,它默认是返回false的,即不删除头节点。如果需要定义是否需要删除头节点的规则,只需覆盖该方法并提供相关实现即可。该方法的作用在于,它提供了当一个新的entry被添加到linkedHashMap中,删除头节点的机会。这是非常有意义的,可以通过删除头节点来减少内存消耗,避免内存溢出。

afterNodeAccess(e)方法

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;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}

该方法在HashMap的putVal方法、LinkedHashMap的get方法中都被调用,它的作用是:如果accessOrder返回值为true(即按照访问顺序迭代),则将最近访问的节点调整至双向队列的队尾,这也就保证了按照访问顺序迭代时Entry的有序性。

get(key)方法

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

该方法增加了按访问顺序或插入顺序进行排序的选择功能,会根据AccessOrder的值调整双向链表中节点的顺序,获取节点的过程与HashMap中一致。

containsValue(value)方法

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

由于LinkedHashMap维护了一个双向链表,因此它的containsValue(value)方法直接遍历双向链表查找对应的Entry即可,而无需去遍历哈希桶。

LinkedHashMap与HashMap

LinkedHashMap是HashMap的子类,它们最大的区别是,HashMap的迭代是无序的,而LinkedHashMap是有序的,并且有按插入顺序和按访问顺序两种方式。为了实现有序迭代,LinkedHashMap相比HashMap,额外维护了一个双向链表,因此一般情况下,遍历HashMap比LinkedHashMap效率要高,在没有按序访问key-value pair的情况下,一般建议使用HashMap(当然也有例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比 LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关)。

------------------------推荐阅读------------------------

2019年JVM最新面试题,必须收藏它

最全面的阿里多线程面试题,你能回答几个?

Java面试题:Java中的集合及其继承关系

花了近十年的时间,整理出史上最全面Java面试题

LinkedHashMap,源码解读就是这么简单的更多相关文章

  1. LinkedHashMap源码解读

    1. 前言 还是从面试中来,到面试中去.面试官在面试 Redis 的时候经常会问到,Redis 的 LRU 是如何实现的?如果让你实现 LRU 算法,你会怎么实现呢?除了用现有的结构 LinkedHa ...

  2. RestTemplate Hashmap变为LinkedHashMap源码解读

    使用restTemplate远程调用服务,正常应该接收List<HashMap>数据,但实际却是List<LikedHashMap>经过不断地debug,终于找到了数据被转换成 ...

  3. HashTable、HashMap与ConCurrentHashMap源码解读

    HashMap 的数据结构 ​ hashMap 初始的数据结构如下图所示,内部维护一个数组,然后数组上维护一个单链表,有个形象的比喻就是想挂钩一样,数组脚标一样的,一个一个的节点往下挂. ​ 我们可以 ...

  4. spring IOC DI AOP MVC 事务, mybatis 源码解读

    demo https://gitee.com/easybao/aop.git spring DI运行时序 AbstractApplicationContext类的 refresh()方法 1: pre ...

  5. SDWebImage源码解读之SDWebImageDownloaderOperation

    第七篇 前言 本篇文章主要讲解下载操作的相关知识,SDWebImageDownloaderOperation的主要任务是把一张图片从服务器下载到内存中.下载数据并不难,如何对下载这一系列的任务进行设计 ...

  6. SDWebImage源码解读 之 NSData+ImageContentType

    第一篇 前言 从今天开始,我将开启一段源码解读的旅途了.在这里先暂时不透露具体解读的源码到底是哪些?因为也可能随着解读的进行会更改计划.但能够肯定的是,这一系列之中肯定会有Swift版本的代码. 说说 ...

  7. SDWebImage源码解读 之 UIImage+GIF

    第二篇 前言 本篇是和GIF相关的一个UIImage的分类.主要提供了三个方法: + (UIImage *)sd_animatedGIFNamed:(NSString *)name ----- 根据名 ...

  8. SDWebImage源码解读_之SDWebImageDecoder

    第四篇 前言 首先,我们要弄明白一个问题? 为什么要对UIImage进行解码呢?难道不能直接使用吗? 其实不解码也是可以使用的,假如说我们通过imageNamed:来加载image,系统默认会在主线程 ...

  9. AFNetworking 3.0 源码解读 总结(干货)(下)

    承接上一篇AFNetworking 3.0 源码解读 总结(干货)(上) 21.网络服务类型NSURLRequestNetworkServiceType 示例代码: typedef NS_ENUM(N ...

随机推荐

  1. Elasticsearch索引按月划分以及获取所有索引数据

    项目中数据库根据月份水平划分,由于没有用数据库中间件,没办法一下查询所有订单信息,所有用Elasticsearch做订单检索. Elasticsearch索引和数据库分片同步,也是根据月份来建立索引. ...

  2. git commit vim报错

    window平台,使用git bash,填写git commit时,退出时vim报错: 错误信息 启动失败 Vim: Caught deadly signal SEGV 在工程目录下,进.git目录, ...

  3. Linux配置svn服务器版本库

    1)创建版本库首先使用yum安装subversion 2)创建版本库 svnadmin create /home/svn/svnfile 3)进入conf目录 authz               ...

  4. jQuery 源码解析(二十八) 样式操作模块 scrollLeft和scrollTop详解

    scrollLeft和scrollTop用于获取/设置滚动条的,如下: scrollLeft(val) ;读取或设置整个页面的水平滚动条距离 scrollTop(val) ;读取或设置整个页面的垂直滚 ...

  5. ThinkPHP调用其他控制器的方法,助手函数action()

    用法: $a = action('admin/user/detail',['id'=>10]); var_dump($a) 表示在其他控制器调用user控制器的detail方法,传了一个参数id

  6. opencv与mfc显示图片操作,MFC的鼠标响应在opencv图片上失效,opencv滚轮事件没有响应问题描述解决。

    原文作者:aircraft 原文链接:https://www.cnblogs.com/DOMLX/p/11588758.html 用mfc 与OPENCV编写成. 出了很多冲突异常事件导致鼠标操作没有 ...

  7. Cesium专栏-空间分析之坡度分析(附源码下载)

    Cesium Cesium 是一款面向三维地球和地图的,世界级的JavaScript开源产品.它提供了基于JavaScript语言的开发包,方便用户快速搭建一款零插件的虚拟地球Web应用,并在性能,精 ...

  8. 查询物料单位PAC成本

    select cpp.period_name 期间名称, ccga.organization_id 组织ID, ood.ORGANIZATION_CODE 组织代码, OOD.ORGANIZATION ...

  9. JS Switch

    JS Switch switch 语句用于基于不同的条件来执行不同的动作. switch(n) { case 1: 执行代码块 1 break; case 2: 执行代码块 2 break; defa ...

  10. 关于Java的多线程Runnable的个人理解(基础,不讲概念)

    背景说明: 在学了Java的多线程(继承Thread,Runnable)以后,我出于好奇,就想知道java到底是不是多线程的,不能它说自己是多线程就是多线程,自己想验证一下,于是我就想测试一下,但继承 ...