一、概述

HashMap是无序的,HashMap在put的时候是根据key的hashcode进行hash然后放入对应的地方。所以在按照一定顺序put进HashMap中,然后遍历出HashMap的顺序跟put的顺序不同(除非在put的时候key已经按照hashcode排序好了,这种几率非常小)

JAVA在JDK1.4以后提供了LinkedHashMap来帮助我们实现了有序的HashMap。

LinkedHashMap是HashMap的一个子类,它保留插入的顺序, 如果需要输出的顺序和输入时的相同,那么就选用LinkedHashMap。

LinkedHashMap是Map接口的哈希表和链接列表实现,具有可预知的迭代顺序。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

LinkedHashMap实现与HashMap的不同之处在于,LinkedHashMap维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。

注意,此实现不是同步的。如果多个线程同时访问链接的哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。

根据链表中元素的顺序可以分为:按插入顺序的链表和按访问顺序(调用get方法)的链表。默认是按插入顺序排序,如果指定按访问顺序排序,那么调用get方法后,会将这次访问的元素移至链表尾部,不断访问可以形成按访问顺序排序的链表。

  1. LinkedHashMap<String, Integer> lmap = new LinkedHashMap<String, Integer>();
  2. lmap.put("语文", 1);
  3. lmap.put("数学", 2);
  4. lmap.put("英语", 3);
  5. lmap.put("历史", 4);
  6. lmap.put("政治", 5);
  7. lmap.put("地理", 6);
  8. lmap.put("生物", 7);
  9. lmap.put("化学", 8);
  10. for(Entry<String, Integer> entry : lmap.entrySet()) {
  11. System.out.println(entry.getKey() + ": " + entry.getValue());
  12. }

运行结果是:

  1. 语文: 1
  2. 数学: 2
  3. 英语: 3
  4. 历史: 4
  5. 政治: 5
  6. 地理: 6
  7. 生物: 7
  8. 化学: 8

我们可以观察到,和HashMap的运行结果不同,LinkedHashMap的迭代输出的结果保持了插入顺序。是什么样的结构使得LinkedHashMap具有如此特性呢?我们还是一样的看看LinkedHashMap的内部结构,对它有一个感性的认识:

Hash table and linked list implementation of the Map interface, with predictable iteration order. This implementation differs from HashMap in that it maintains a doubly-linked list running through all of its entries. This linked list defines the iteration ordering, which is normally the order in which keys were inserted into the map (insertion-order).

正如官方文档所说:LinkedHashMap是Hash表和链表的实现,并且依靠着双向链表保证了迭代顺序是插入的顺序。

二、插入顺序、访问顺序的演示

先做几个demo来演示一下LinkedHashMap的使用。看懂了其效果,然后再来研究其原理。

2.1 HashMap

看下面这个代码:

  1. public static void main(String[] args) {
  2. Map<String, String> map = new HashMap<String, String>();
  3. map.put("apple", "苹果");
  4. map.put("watermelon", "西瓜");
  5. map.put("banana", "香蕉");
  6. map.put("peach", "桃子");
  7. Iterator iter = map.entrySet().iterator();
  8. while (iter.hasNext()) {
  9. Map.Entry entry = (Map.Entry) iter.next();
  10. System.out.println(entry.getKey() + "=" + entry.getValue());
  11. }
  12. }

一个比较简单的测试HashMap的代码,通过控制台的输出,我们可以看到HashMap是没有顺序的。

  1. banana=香蕉
  2. apple=苹果
  3. peach=桃子
  4. watermelon=西瓜

2.2 LinkedHashMap

我们现在将map的实现换成LinkedHashMap,其他代码不变:

Map<String, String> map = new LinkedHashMap<String, String>();

看一下控制台的输出:

  1. apple=苹果
  2. watermelon=西瓜
  3. banana=香蕉
  4. peach=桃子

我们可以看到,其输出顺序是完成按照插入顺序的!也就是我们上面所说的保留了插入的顺序。我们不是在上面还提到过其可以按照访问顺序进行排序么?好的,我们还是通过一个例子来验证一下:

  1. public static void main(String[] args) {
  2. Map<String, String> map = new LinkedHashMap<String, String>(16,0.75f,true);
  3. map.put("apple", "苹果");
  4. map.put("watermelon", "西瓜");
  5. map.put("banana", "香蕉");
  6. map.put("peach", "桃子");
  7. map.get("banana");
  8. map.get("apple");
  9. Iterator iter = map.entrySet().iterator();
  10. while (iter.hasNext()) {
  11. Map.Entry entry = (Map.Entry) iter.next();
  12. System.out.println(entry.getKey() + "=" + entry.getValue());
  13. }
  14. }

代码与之前的都差不多,但我们多了两行代码,并且初始化LinkedHashMap的时候,用的构造函数也不相同,看一下控制台的输出结果:

  1. watermelon=西瓜
  2. peach=桃子
  3. banana=香蕉
  4. apple=苹果

这也就是我们之前提到过的,LinkedHashMap可以选择按照访问顺序进行排序。

三、LinkedHashMap的实现

对于LinkedHashMap而言,它继承于HashMap、底层使用哈希表与双向链表来保存所有元素。其基本操作与父类HashMap相似,它通过重写父类相关的方法,来实现自己的链接列表特性。下面我们来分析LinkedHashMap的源代码:

3.1 成员变量

LinkedHashMap采用的hash算法和HashMap相同,但是它重新定义了数组中保存的元素Entry,该Entry除了保存当前对象的引用外,还保存了其上一个元素before和下一个元素after的引用,从而在哈希表的基础上又构成了双向链接列表。看源代码:

  1. /**
  2. * The iteration ordering method for this linked hash map: <tt>true</tt>
  3. * for access-order, <tt>false</tt> for insertion-order.
  4. * 如果为true,则按照访问顺序;如果为false,则按照插入顺序。
  5. */
  6. private final boolean accessOrder;
  7. /**
  8. * 双向链表的表头元素。
  9. */
  10. private transient Entry<K,V> header;
  11. /**
  12. * LinkedHashMap的Entry元素。
  13. * 继承HashMap的Entry元素,又保存了其上一个元素before和下一个元素after的引用。
  14. */
  15. private static class Entry<K,V> extends HashMap.Entry<K,V> {
  16. Entry<K,V> before, after;
  17. ……
  18. }

LinkedHashMap中的Entry集成与HashMap的Entry,但是其增加了before和after的引用,指的是上一个元素和下一个元素的引用。

3.2 初始化

通过源代码可以看出,在LinkedHashMap的构造方法中,实际调用了父类HashMap的相关构造方法来构造一个底层存放的table数组,但额外可以增加accessOrder这个参数,如果不设置,默认为false,代表按照插入顺序进行迭代;当然可以显式设置为true,代表以访问顺序进行迭代。如:

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

HashMap中的相关构造方法:

  1. public HashMap(int initialCapacity, float loadFactor) {
  2. if (initialCapacity < 0)
  3. throw new IllegalArgumentException("Illegal initial capacity: " +
  4. initialCapacity);
  5. if (initialCapacity > MAXIMUM_CAPACITY)
  6. initialCapacity = MAXIMUM_CAPACITY;
  7. if (loadFactor <= 0 || Float.isNaN(loadFactor))
  8. throw new IllegalArgumentException("Illegal load factor: " +
  9. loadFactor);
  10.  
  11. // Find a power of 2 >= initialCapacity
  12. int capacity = 1;
  13. while (capacity < initialCapacity)
  14. capacity <<= 1;
  15.  
  16. this.loadFactor = loadFactor;
  17. threshold = (int)(capacity * loadFactor);
  18. table = new Entry[capacity];
  19. init();
  20. }

我们已经知道LinkedHashMap的Entry元素继承HashMap的Entry,提供了双向链表的功能。在上述HashMap的构造器
中,最后会调用init()方法,进行相关的初始化,这个方法在HashMap的实现中并无意义,只是提供给子类实现相关的初始化调用。

LinkedHashMap重写了init()方法,在调用父类的构造方法完成构造后,进一步实现了对其元素Entry的初始化操作。

  1. /**
  2. * Called by superclass constructors and pseudoconstructors (clone,
  3. * readObject) before any entries are inserted into the map. Initializes
  4. * the chain.
  5. */
  6. @Override
  7. void init() {
  8. header = new Entry<>(-1, null, null, null);
  9. header.before = header.after = header;
  10. }

3.3 存储

LinkedHashMap并未重写父类HashMap的put方法,而是重写了父类HashMap的put方法调用的子方法void recordAccess(HashMap m) ,void addEntry(int hash, K key, V value, int bucketIndex) 和void createEntry(int hash, K key, V value, int bucketIndex),提供了自己特有的双向链接列表的实现。我们在之前的文章中已经讲解了HashMap的put方法,我们在这里重新贴一下HashMap的put方法的源代码:

  1. public V put(K key, V value) {
  2. if (key == null)
  3. return putForNullKey(value);
  4. int hash = hash(key);
  5. int i = indexFor(hash, table.length);
  6. for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  7. Object k;
  8. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  9. V oldValue = e.value;
  10. e.value = value;
  11. e.recordAccess(this);
  12. return oldValue;
  13. }
  14. }
  15. modCount++;
  16. addEntry(hash, key, value, i);
  17. return null;
  18. }

重写方法:

  1. void recordAccess(HashMap<K,V> m) {
  2. LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
  3. if (lm.accessOrder) {
  4. lm.modCount++;
  5. remove();
  6. addBefore(lm.header);
  7. }
  8. }
  9. void addEntry(int hash, K key, V value, int bucketIndex) {
  10. // 调用create方法,将新元素以双向链表的的形式加入到映射中。
  11. createEntry(hash, key, value, bucketIndex);
  12. // 删除最近最少使用元素的策略定义
  13. Entry<K,V> eldest = header.after;
  14. if (removeEldestEntry(eldest)) {
  15. removeEntryForKey(eldest.key);
  16. } else {
  17. if (size >= threshold)
  18. resize(2 * table.length);
  19. }
  20. }
  21. void createEntry(int hash, K key, V value, int bucketIndex) {
  22. HashMap.Entry<K,V> old = table[bucketIndex];
  23. Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
  24. table[bucketIndex] = e;
  25. // 调用元素的addBrefore方法,将元素加入到哈希、双向链接列表。
  26. e.addBefore(header);
  27. size++;
  28. }
  29. private void addBefore(Entry<K,V> existingEntry) {
  30. after = existingEntry;
  31. before = existingEntry.before;
  32. before.after = this;
  33. after.before = this;
  34. }

3.4 读取

LinkedHashMap重写了父类HashMap的get方法,实际在调用父类getEntry()方法取得查找的元素后,再判断当排序模式accessOrder为true时,记录访问顺序,将最新访问的元素添加到双向链表的表头,并从原来的位置删除。由于的链表的增加、删除操作是常量级的,故并不会带来性能的损失。

  1. public V get(Object key) {
  2. // 调用父类HashMap的getEntry()方法,取得要查找的元素。
  3. Entry<K,V> e = (Entry<K,V>)getEntry(key);
  4. if (e == null)
  5. return null;
  6. // 记录访问顺序。
  7. e.recordAccess(this);
  8. return e.value;
  9. }
  10. void recordAccess(HashMap<K,V> m) {
  11. LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
  12. // 如果定义了LinkedHashMap的迭代顺序为访问顺序,
  13. // 则删除以前位置上的元素,并将最新访问的元素添加到链表表头。
  14. if (lm.accessOrder) {
  15. lm.modCount++;
  16. remove();
  17. addBefore(lm.header);
  18. }
  19. }
  20. /**
  21. * Removes this entry from the linked list.
  22. */
  23. private void remove() {
  24. before.after = after;
  25. after.before = before;
  26. }
  27. /**clear链表,设置header为初始状态*/
  28. public void clear() {
  29. super.clear();
  30. header.before = header.after = header;
  31. }

LinkedHashMap的这些额外操作基本上都是为了维护好那个具有访问顺序的双向链表,目的就是保持双向链表中节点的顺序要从eldest到youngest。

3.4 排序模式

LinkedHashMap定义了排序模式accessOrder,该属性为boolean型变量,对于访问顺序,为true;对于插入顺序,则为false。

  1. private final boolean accessOrder;

一般情况下,不必指定排序模式,其迭代顺序即为默认为插入顺序。看LinkedHashMap的构造方法,如:

  1. public LinkedHashMap(int initialCapacity, float loadFactor) {
  2. super(initialCapacity, loadFactor);
  3. accessOrder = false;
  4. }

这些构造方法都会默认指定排序模式为插入顺序。如果你想构造一个LinkedHashMap,并打算按从近期访问最少到近期访问最多的顺序(即访问顺序)来保存元素,那么请使用下面的构造方法构造LinkedHashMap:

  1. public LinkedHashMap(int initialCapacity,
  2. float loadFactor,
  3. boolean accessOrder) {
  4. super(initialCapacity, loadFactor);
  5. this.accessOrder = accessOrder;
  6. }

该哈希映射的迭代顺序就是最后访问其条目的顺序,这种映射很适合构建LRU缓存。LinkedHashMap提供了removeEldestEntry(Map.Entry<k,v> eldest)方法,在将新条目插入到映射后,put和 putAll将调用此方法。该方法可以提供在每次添加新条目时移除最旧条目的实现程序,默认返回false,这样,此映射的行为将类似于正常映射,即永远不能移除最旧的元素。

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

此方法通常不以任何方式修改映射,相反允许映射在其返回值的指引下进行自我修改。如果用此映射构建LRU缓存,则非常方便,它允许映射通过删除旧条目来减少内存损耗。

例如:重写此方法,维持此映射只保存100个条目的稳定状态,在每次添加新条目时删除最旧的条目。

  1. private static final int MAX_ENTRIES = 100;
  2. protected boolean removeEldestEntry(Map.Entry eldest) {
  3. return size() > MAX_ENTRIES;
  4. }

四、总结

其实LinkedHashMap几乎和HashMap一样:从技术上来说,不同的是它定义了一个Entry header,这个header不是放在Table里,它是额外独立出来的。LinkedHashMap通过继承hashMap中的Entry,并添加两个属性Entry before,after,和header结合起来组成一个双向链表,来实现按插入顺序或访问顺序排序。

在写关于LinkedHashMap的过程中,记起来之前面试的过程中遇到的一个问题,也是问我Map的哪种实现可以做到按照插入顺序进行迭代?当时脑子是突然短路的,但现在想想,也只能怪自己对这个知识点还是掌握的不够扎实,所以又从头认真的把代码看了一遍。

不过,我的建议是,大家首先首先需要记住的是:LinkedHashMap能够做到按照插入顺序或者访问顺序进行迭代,这样在我们以后的开发中遇到相似的问题,才能想到用LinkedHashMap来解决,否则就算对其内部结构非常了解,不去使用也是没有什么用的。我们学习的目的是为了更好的应用。

Java集合学习(5):LinkedHashMap的更多相关文章

  1. 转:深入Java集合学习系列:HashSet的实现原理

    0.参考文献 深入Java集合学习系列:HashSet的实现原理 1.HashSet概述: HashSet实现Set接口,由哈希表(实际上是一个HashMap实例)支持.它不保证set 的迭代顺序:特 ...

  2. 2019/3/4 java集合学习(二)

    java集合学习(二) 在学完ArrayList 和 LinkedList之后,基本已经掌握了最基本的java常用数据结构,但是为了提高程序的效率,还有很多种特点各异的数据结构等着我们去运用,类如可以 ...

  3. 2019/3/2周末 java集合学习(一)

    Java集合学习(一) ArraysList ArraysList集合就像C++中的vector容器,它可以不考虑其容器的长度,就像一个大染缸一 样,无穷无尽的丢进去也没问题.Java的数据结构和C有 ...

  4. Java集合学习(9):集合对比

    一.HashMap与HashTable的区别 HashMap和Hashtable的比较是Java面试中的常见问题,用来考验程序员是否能够正确使用集合类以及是否可以随机应变使用多种思路解决问题.Hash ...

  5. java集合学习(2):Map和HashMap

    Map接口 java.util 中的集合类包含 Java 中某些最常用的类.最常用的集合类是 List 和 Map. Map 是一种键-值对(key-value)集合,Map 集合中的每一个元素都包含 ...

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

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

  7. Java集合中的LinkedHashMap类

    jdk1.8.0_144 本文阅读最好先了解HashMap底层,可前往<Java集合中的HashMap类>. LinkedHashMap由于它的插入有序特性,也是一种比较常用的Map集合. ...

  8. Java集合学习总结

    java集合 collection public interface Collection<E> extends Iterable<E> List public interfa ...

  9. java集合学习(1):集合框架

    集合 Collection(有时候也叫container)是一个简单的对象, Java集合工具包位于Java.util包下,Java集合主要可以划分为4个部分:List列表.Set集合.Map映射.工 ...

  10. 深入java集合学习1-集合框架浅析

    前言 集合是一种数据结构,在编程中是非常重要的.好的程序就是好的数据结构+好的算法.java中为我们实现了曾经在大学学过的数据结构与算法中提到的一些数据结构.如顺序表,链表,栈和堆等.Java 集合框 ...

随机推荐

  1. Flex弹性盒模型(新老版本完整)--移动端开发整理笔记(二)

    Flex布局 Flex即Flexible Box,写法为:display:flex(旧版:display: -webkit-box) 在Webkit内核下,需要加-webkit前缀: .box{ di ...

  2. unzip命令(转)

    unzip命令用于解压缩由zip命令压缩的“.zip”压缩包. 语法 unzip(选项)(参数) 选项 -c:将解压缩的结果显示到屏幕上,并对字符做适当的转换: -f:更新现有的文件: -l:显示压缩 ...

  3. 三天精通Vue--Vue的常用语法

    Vue的介绍 官网教程:https://cn.vuejs.org/v2/guide/installation.html 掘金:https://juejin.im/ cdn(在线的网络连接资源):htt ...

  4. [no_perms] Private mode enable, only admin can publish this module

    在使用npm publish是出现了错误: npm ERR! code E403 npm ERR! 403 Forbidden - PUT https://registry.npm.taobao.or ...

  5. redis实现mysql的数据缓存

    环境设定base2 172.25.78.12 nginx+phpbase3 172.25.78.13 redis端base4 172.25.78.14 mysql端# 1.在base2(nginx+p ...

  6. dockerfile 的问题 FROM alpine:3.8 temporary error (try again later)

    FROM alpine:3.8 apk add xxx安装软件 fetch http://dl-cdn.alpinelinux.org/alpine/v3.8/main/x86_64/APKINDEX ...

  7. maven 向私服部署jar

    1.有源码的情况下 首先需要在要deploy的项目pom中添加私服地址 <distributionManagement> <repository> <id>nexu ...

  8. windows上svn图标不显示 绿色对号

    http://blog.csdn.net/fengyupeng/article/details/12514449 症状1:项目左侧导航栏表不能正常显示图标 方法:windows->prefere ...

  9. linux lnmp安装2个版本PHP教程

    linux lnmp安装2个版本PHP教程我原先装了5.6版本的PHP 后来想装个PHP7.0.14版本 一方面看看稳定性 另一方面看看性能怎么样 其实原理很简单 php-fpm开启了1个端口来管理P ...

  10. [转帖]算法精解:DAG有向无环图

    算法精解:DAG有向无环图 https://www.cnblogs.com/Evsward/p/dag.html DAG是公认的下一代区块链的标志.本文从算法基础去研究分析DAG算法,以及它是如何运用 ...