首页 Java java教程 详解LinkedHashMap如何保证元素迭代的顺序

详解LinkedHashMap如何保证元素迭代的顺序

May 12, 2017 am 09:38 AM

本文主要介绍了Java中LinkedHashMap的相关知识,具有很好的参考价值。下面跟着小编一起来看下吧

初识LinkedHashMap

大多数情况下,只要不涉及线程安全问题,Map基本都可以使用HashMap,不过HashMap有一个问题,就是迭代HashMap的顺序并不是HashMap放置的顺序,也就是无序。HashMap的这一缺点往往会带来困扰,因为有些场景,我们期待一个有序的Map。

这个时候,LinkedHashMap就闪亮登场了,它虽然增加了时间和空间上的开销,但是通过维护一个运行于所有条目的双向链表,LinkedHashMap保证了元素迭代的顺序。

四个关注点在LinkedHashMap上的答案

关  注  点 结      论
LinkedHashMap是否允许空 Key和Value都允许空
LinkedHashMap是否允许重复数据 Key重复会覆盖、Value允许重复
LinkedHashMap是否有序 有序
LinkedHashMap是否线程安全 非线程安全

LinkedHashMap基本结构

关于LinkedHashMap,先提两点:

1、LinkedHashMap可以认为是HashMap+LinkedList,即它既使用HashMap操作数据结构,又使用LinkedList维护插入元素的先后顺序

2、LinkedHashMap的基本实现思想就是----多态。可以说,理解多态,再去理解LinkedHashMap原理会事半功倍;反之也是,对于LinkedHashMap原理的学习,也可以促进和加深对于多态的理解。

为什么可以这么说,首先看一下,LinkedHashMap的定义:

public class LinkedHashMap<K,V>
 extends HashMap<K,V>
 implements Map<K,V>
{
 ...
}
登录后复制

看到,LinkedHashMap是HashMap的子类,自然LinkedHashMap也就继承了HashMap中所有非private的方法。再看一下LinkedHashMap中本身的方法:

看到LinkedHashMap中并没有什么操作数据结构的方法,也就是说LinkedHashMap操作数据结构(比如put一个数据),和HashMap操作数据的方法完全一样,无非就是细节上有一些的不同罢了。

LinkedHashMap和HashMap的区别在于它们的基本数据结构上,看一下LinkedHashMap的基本数据结构,也就是Entry:

private static class Entry<K,V> extends HashMap.Entry<K,V> {
 // These fields comprise the doubly linked list used for iteration.
 Entry<K,V> before, after;
Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
  super(hash, key, value, next);
 }
 ...
}
登录后复制

列一下Entry里面有的一些属性吧:

  • K key

  • V value

  • Entry next

  • int hash

  • Entry before

  • Entry after

其中前面四个,也就是红色部分是从HashMap.Entry中继承过来的;后面两个,也就是蓝色部分是LinkedHashMap独有的。不要搞错了next和before、After,next是用于维护HashMap指定table位置上连接的Entry的顺序的,before、After是用于维护Entry插入的先后顺序的。

还是用图表示一下,列一下属性而已:

初始化LinkedHashMap

假如有这么一段代码:

public static void main(String[] args)
{
 LinkedHashMap<String, String> linkedHashMap =
   new LinkedHashMap<String, String>();
 linkedHashMap.put("111", "111");
 linkedHashMap.put("222", "222");
}
登录后复制

首先是第3行~第4行,new一个LinkedHashMap出来,看一下做了什么:

public LinkedHashMap() {
 super();
  accessOrder = false;
 }
登录后复制
 public HashMap() {
 this.loadFactor = DEFAULT_LOAD_FACTOR;
  threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
  table = new Entry[DEFAULT_INITIAL_CAPACITY];
  init();
 }
登录后复制
void init() {
 header = new Entry<K,V>(-1, null, null, null);
 header.before = header.after = header;
}
登录后复制
/**
 * The head of the doubly linked list.
 */
private transient Entry<K,V> header;
登录后复制

这里出现了第一个多态:init()方法。尽管init()方法定义在HashMap中,但是由于:

1、LinkedHashMap重写了init方法

2、实例化出来的是LinkedHashMap

因此实际调用的init方法是LinkedHashMap重写的init方法。假设header的地址是0x00000000,那么初始化完毕,实际上是这样的:

LinkedHashMap添加元素

继续看LinkedHashMap添加元素,也就是put("111","111")做了什么,首先当然是调用HashMap的put方法:

public V put(K key, V value) {
 if (key == null)
  return putForNullKey(value);
 int hash = hash(key.hashCode());
 int i = indexFor(hash, table.length);
 for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  Object k;
  if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
   V oldValue = e.value;
   e.value = value;
   e.recordAccess(this);
   return oldValue;
  }
 }
 modCount++;
 addEntry(hash, key, value, i);
 return null;
}
登录后复制
登录后复制

第17行又是一个多态,因为LinkedHashMap重写了addEntry方法,因此addEntry调用的是LinkedHashMap重写了的方法:

void addEntry(int hash, K key, V value, int bucketIndex) {
 createEntry(hash, key, value, bucketIndex);

 // Remove eldest entry if instructed, else grow capacity if appropriate
 Entry<K,V> eldest = header.after;
 if (removeEldestEntry(eldest)) {
  removeEntryForKey(eldest.key);
 } else {
  if (size >= threshold)
   resize(2 * table.length);
 }
}
登录后复制

因为LinkedHashMap由于其本身维护了插入的先后顺序,因此LinkedHashMap可以用来做缓存,第5行~第7行是用来支持FIFO算法的,这里暂时不用去关心它。看一下createEntry方法:


void createEntry(int hash, K key, V value, int bucketIndex) {
 HashMap.Entry<K,V> old = table[bucketIndex];
 Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
 table[bucketIndex] = e;
 e.addBefore(header);
 size++;
}
登录后复制


private void addBefore(Entry<K,V> existingEntry) {
 after = existingEntry;
 before = existingEntry.before;
 before.after = this;
 after.before = this;
}
登录后复制
登录后复制

第2行~第4行的代码和HashMap没有什么不同,新添加的元素放在table[i]上,差别在于LinkedHashMap还做了addBefore操作,这四行代码的意思就是让新的Entry和原链表生成一个双向链表。假设字符串111放在位置table[1]上,生成的Entry地址为0x00000001,那么用图表示是这样的:

如果熟悉LinkedList的源码应该不难理解,还是解释一下,注意下existingEntry表示的是header:

1、after=existingEntry,即新增的Entry的after=header地址,即after=0x00000000

2、before=existingEntry.before,即新增的Entry的before是header的before的地址,header的before此时是0x00000000,因此新增的Entry的before=0x00000000

3、before.after=this,新增的Entry的before此时为0x00000000即header,header的after=this,即header的after=0x00000001

4、after.before=this,新增的Entry的after此时为0x00000000即header,header的before=this,即header的before=0x00000001

这样,header与新增的Entry的一个双向链表就形成了。再看,新增了字符串222之后是什么样的,假设新增的Entry的地址为0x00000002,生成到table[2]上,用图表示是这样的:

就不细解释了,只要before、after清除地知道代表的是哪个Entry的就不会有什么问题。

总得来看,再说明一遍,LinkedHashMap的实现就是HashMap+LinkedList的实现方式,以HashMap维护数据结构,以LinkList的方式维护数据插入顺序。

利用LinkedHashMap实现LRU算法缓存

前面讲了LinkedHashMap添加元素,删除、修改元素就不说了,比较简单,和HashMap+LinkedList的删除、修改元素大同小异,下面讲一个新的内容。

LinkedHashMap可以用来作缓存,比方说LRUCache,看一下这个类的代码,很简单,就十几行而已:

public class LRUCache extends LinkedHashMap
{
 public LRUCache(int maxSize)
 {
  super(maxSize, 0.75F, true);
  maxElements = maxSize;
 }

 protected boolean removeEldestEntry(java.util.Map.Entry eldest)
 {
  return size() > maxElements;
 }
 private static final long serialVersionUID = 1L;
 protected int maxElements;
}
登录后复制

顾名思义,LRUCache就是基于LRU算法的Cache(缓存),这个类继承自LinkedHashMap,而类中看到没有什么特别的方法,这说明LRUCache实现缓存LRU功能都是源自LinkedHashMap的。LinkedHashMap可以实现LRU算法的缓存基于两点:

1、LinkedList首先它是一个Map,Map是基于K-V的,和缓存一致

2、LinkedList提供了一个boolean值可以让用户指定是否实现LRU

那么,首先我们了解一下什么是LRU:LRU即Least Recently Used,最近最少使用,也就是说,当缓存满了,会优先淘汰那些最近最不常访问的数据。比方说数据a,1天前访问了;数据b,2天前访问了,缓存满了,优先会淘汰数据b。

我们看一下LinkedList带boolean型参数的构造方法

public LinkedHashMap(int initialCapacity,
   float loadFactor,
      boolean accessOrder) {
 super(initialCapacity, loadFactor);
 this.accessOrder = accessOrder;
}
登录后复制

就是这个accessOrder,它表示:

(1)false,所有的Entry按照插入的顺序排列

(2)true,所有的Entry按照访问的顺序排列

第二点的意思就是,如果有1 2 3这3个Entry,那么访问了1,就把1移到尾部去,即2 3 1。每次访问都把访问的那个数据移到双向队列的尾部去,那么每次要淘汰数据的时候,双向队列最头的那个数据不就是最不常访问的那个数据了吗?换句话说,双向链表最头的那个数据就是要淘汰的数据。

"访问",这个词有两层意思:

1、根据Key拿到Value,也就是get方法

2、修改Key对应的Value,也就是put方法

首先看一下get方法,它在LinkedHashMap中被重写:

public V get(Object key) {
 Entry<K,V> e = (Entry<K,V>)getEntry(key);
 if (e == null)
  return null;
 e.recordAccess(this);
 return e.value;
}
登录后复制

然后是put方法,沿用父类HashMap的:

public V put(K key, V value) {
 if (key == null)
  return putForNullKey(value);
 int hash = hash(key.hashCode());
 int i = indexFor(hash, table.length);
 for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  Object k;
  if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
   V oldValue = e.value;
   e.value = value;
   e.recordAccess(this);
   return oldValue;
  }
 }
 modCount++;
 addEntry(hash, key, value, i);
 return null;
}
登录后复制
登录后复制

修改数据也就是第6行~第14行的代码。看到两端代码都有一个共同点:都调用了recordAccess方法,且这个方法是Entry中的方法,也就是说每次的recordAccess操作的都是某一个固定的Entry。

recordAccess,顾名思义,记录访问,也就是说你这次访问了双向链表,我就把你记录下来,怎么记录?把你访问的Entry移到尾部去。这个方法在HashMap中是一个空方法,就是用来给子类记录访问用的,看一下LinkedHashMap中的实现:

void recordAccess(HashMap<K,V> m) {
 LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
 if (lm.accessOrder) {
  lm.modCount++;
  remove();
  addBefore(lm.header);
 }
}
登录后复制
private void remove() {
 before.after = after;
 after.before = before;
}
登录后复制
private void addBefore(Entry<K,V> existingEntry) {
 after = existingEntry;
 before = existingEntry.before;
 before.after = this;
 after.before = this;
}
登录后复制
登录后复制

看到每次recordAccess的时候做了两件事情:

1、把待移动的Entry的前后Entry相连

2、把待移动的Entry移动到尾部

当然,这一切都是基于accessOrder=true的情况下。最后用一张图表示一下整个recordAccess的过程吧:

代码演示LinkedHashMap按照访问顺序排序的效果

最后代码演示一下LinkedList按照访问顺序排序的效果,验证一下上一部分LinkedHashMap的LRU功能:

public static void main(String[] args)
{
 LinkedHashMap<String, String> linkedHashMap =
   new LinkedHashMap<String, String>(16, 0.75f, true);
 linkedHashMap.put("111", "111");
 linkedHashMap.put("222", "222");
 linkedHashMap.put("333", "333");
 linkedHashMap.put("444", "444");
 loopLinkedHashMap(linkedHashMap);
 linkedHashMap.get("111");
 loopLinkedHashMap(linkedHashMap);
 linkedHashMap.put("222", "2222");
 loopLinkedHashMap(linkedHashMap);
}
 
public static void loopLinkedHashMap(LinkedHashMap<String, String> linkedHashMap)
{
 Set<Map.Entry<String, String>> set = inkedHashMap.entrySet();
 Iterator<Map.Entry<String, String>> iterator = set.iterator();
 
 while (iterator.hasNext())
 {
  System.out.print(iterator.next() + "\t");
 }
 System.out.println();
}
登录后复制

注意这里的构造方法要用三个参数那个且最后的要传入true,这样才表示按照访问顺序排序。看一下代码运行结果:

111=111 222=222 333=333 444=444 
222=222 333=333 444=444 111=111 
333=333 444=444 111=111 222=2222
登录后复制

代码运行结果证明了两点:

1、LinkedList是有序的

2、每次访问一个元素(get或put),被访问的元素都被提到最后面去了

【相关推荐】

1. Java免费视频教程

2. 全面解析Java注解

3. Java教程手册

以上是详解LinkedHashMap如何保证元素迭代的顺序的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

<🎜>:泡泡胶模拟器无穷大 - 如何获取和使用皇家钥匙
4 周前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系统,解释
4 周前 By 尊渡假赌尊渡假赌尊渡假赌
Mandragora:巫婆树的耳语 - 如何解锁抓钩
3 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

热门话题

Java教程
1673
14
CakePHP 教程
1428
52
Laravel 教程
1333
25
PHP教程
1278
29
C# 教程
1257
24
PHP与Python:了解差异 PHP与Python:了解差异 Apr 11, 2025 am 12:15 AM

PHP和Python各有优势,选择应基于项目需求。1.PHP适合web开发,语法简单,执行效率高。2.Python适用于数据科学和机器学习,语法简洁,库丰富。

PHP:网络开发的关键语言 PHP:网络开发的关键语言 Apr 13, 2025 am 12:08 AM

PHP是一种广泛应用于服务器端的脚本语言,特别适合web开发。1.PHP可以嵌入HTML,处理HTTP请求和响应,支持多种数据库。2.PHP用于生成动态网页内容,处理表单数据,访问数据库等,具有强大的社区支持和开源资源。3.PHP是解释型语言,执行过程包括词法分析、语法分析、编译和执行。4.PHP可以与MySQL结合用于用户注册系统等高级应用。5.调试PHP时,可使用error_reporting()和var_dump()等函数。6.优化PHP代码可通过缓存机制、优化数据库查询和使用内置函数。7

突破或从Java 8流返回? 突破或从Java 8流返回? Feb 07, 2025 pm 12:09 PM

Java 8引入了Stream API,提供了一种强大且表达力丰富的处理数据集合的方式。然而,使用Stream时,一个常见问题是:如何从forEach操作中中断或返回? 传统循环允许提前中断或返回,但Stream的forEach方法并不直接支持这种方式。本文将解释原因,并探讨在Stream处理系统中实现提前终止的替代方法。 延伸阅读: Java Stream API改进 理解Stream forEach forEach方法是一个终端操作,它对Stream中的每个元素执行一个操作。它的设计意图是处

PHP与其他语言:比较 PHP与其他语言:比较 Apr 13, 2025 am 12:19 AM

PHP适合web开发,特别是在快速开发和处理动态内容方面表现出色,但不擅长数据科学和企业级应用。与Python相比,PHP在web开发中更具优势,但在数据科学领域不如Python;与Java相比,PHP在企业级应用中表现较差,但在web开发中更灵活;与JavaScript相比,PHP在后端开发中更简洁,但在前端开发中不如JavaScript。

PHP与Python:核心功能 PHP与Python:核心功能 Apr 13, 2025 am 12:16 AM

PHP和Python各有优势,适合不同场景。1.PHP适用于web开发,提供内置web服务器和丰富函数库。2.Python适合数据科学和机器学习,语法简洁且有强大标准库。选择时应根据项目需求决定。

PHP的影响:网络开发及以后 PHP的影响:网络开发及以后 Apr 18, 2025 am 12:10 AM

PHPhassignificantlyimpactedwebdevelopmentandextendsbeyondit.1)ItpowersmajorplatformslikeWordPressandexcelsindatabaseinteractions.2)PHP'sadaptabilityallowsittoscaleforlargeapplicationsusingframeworkslikeLaravel.3)Beyondweb,PHPisusedincommand-linescrip

PHP:许多网站的基础 PHP:许多网站的基础 Apr 13, 2025 am 12:07 AM

PHP成为许多网站首选技术栈的原因包括其易用性、强大社区支持和广泛应用。1)易于学习和使用,适合初学者。2)拥有庞大的开发者社区,资源丰富。3)广泛应用于WordPress、Drupal等平台。4)与Web服务器紧密集成,简化开发部署。

PHP与Python:用例和应用程序 PHP与Python:用例和应用程序 Apr 17, 2025 am 12:23 AM

PHP适用于Web开发和内容管理系统,Python适合数据科学、机器学习和自动化脚本。1.PHP在构建快速、可扩展的网站和应用程序方面表现出色,常用于WordPress等CMS。2.Python在数据科学和机器学习领域表现卓越,拥有丰富的库如NumPy和TensorFlow。

See all articles