Java基础 LinkedHashMap

LinkedHashMap

  • LinkedHashMap
    • 定义
      • LinkedHashMap的原理图
      • LinkedHashMap和HashMap的Entry结构图
    • LinkedHashMap在JDK中的定义
      • LinkedHashMap继承关系:
      • LinkedHashMap成员变量
      • LinkedHashMap构造方法(5种)
      • LinkedHashMap的init()方法
      • LinkedHashMap基本数据结构(Entry:具体结构图在定义中)
      • LinkedHashMap(Map<? extends K, ? extends V> m)
    • LinkedHashMap的快速存取
      • LinkedHashMap 的存储实现 : put(key, vlaue)
      • LinkedHashMap 的读取实现 :get(Object key)
    • LinkeList与LRU(Least recently used,最近最少使用)算法
      • 使用LinkedList实现LRU算法
      • LinkedList有序性原理分析
    • 总结

LinkedHashMap

定义

LinkedHashMap是HashMap和双向链表的合二为一,即一个将所有Entry节点链入一个双向链表的HashMap(LinkedHashMap = HashMap + 双向链表)

  • LinkedHashMap和HashMap是Java Collection Framework 的重要成员,也是Map族(如下图所示)
  • LinkedHashMap是HashMap的子类(拥有HashMap的所有特性)
  • LinkedHashMap和HashMap最多只允许一条Entry的键为Null(多条会覆盖),但允许多条Entry的值为Null
  • LinkedHashMap 也是 Map 的一个非同步的实现
  • LinkedHashMap很好的支持LRU算法
  • HashMap是无序的,LinkedHashMap通过维护一个额外的双向链表保证了迭代顺序
  • 迭代顺序可以是插入顺序,也可以是访问顺序(即根据链表中元素的顺序可以将LinkedHashMap分为:保持插入顺序的LinkedHashMap和保持访问顺序的LinkedHashMap,其中LinkedHashMap的默认实现是按插入顺序排序的)

在这里插入图片描述

LinkedHashMap的原理图

在这里插入图片描述

LinkedHashMap和HashMap的Entry结构图

在这里插入图片描述

LinkedHashMap和HashMap都是Java中常用的数据结构,它们都实现了Map接口。
HashMap是一种基于哈希表实现的无序映射,它通过将键值对存储在哈希表中来实现快速查找和插入操作。HashMap使用哈希函数将键映射到哈希表中的一个位置,然后将键值对存储在该位置上。由于哈希表是无序的,因此HashMap不保证元素的顺序。
LinkedHashMap则是一种有序映射,它继承自HashMap并添加了一个双向链表来维护元素的插入顺序或访问顺序。当向LinkedHashMap中添加元素时,会将该元素添加到链表的末尾;当访问某个元素时,会将该元素移动到链表的末尾。这样,遍历LinkedHashMap时,就可以按照元素的插入顺序或访问顺序进行遍历。
总的来说,HashMap和LinkedHashMap的主要区别在于是否维护元素的顺序。HashMap是无序的,而LinkedHashMap是有序的。

LinkedHashMap在JDK中的定义

LinkedHashMap继承关系:

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

LinkedHashMap成员变量

  • 相比于Hashmap,LinkedHashMap新增 双向链表头结点header和标志位accessOrder
  • accessOrder
    • 默认为false(即默认按照插入顺序迭代)
    • 为true时(按照访问顺序迭代,支持实现LRU算法时)
private static final long serialVersionUID = 3801124242820219131L;
private transient Entry<K,V> header;//双向链表头节点,也即哨兵节点,里面不存储任何信息
private final boolean accessOrder;//有序性标识

LinkedHashMap构造方法(5种)

  • 相比于Hashmap,LinkedHashMap并没有增加构造方法
//传入的参数为初始容量,加载因子,调用了父类的构造方法,按照插入顺序
public LinkedHashMap(int initialCapacity, float loadFactor) {super(initialCapacity, loadFactor);accessOrder = false;}//传入的参数的初始容量,调用父类的构造方法,取得键值对的顺序是插入顺序
public LinkedHashMap(int initialCapacity) {super(initialCapacity);accessOrder = false;}//无参构造,调用父类的构造方法,取得键值对的顺序是插入顺序
public LinkedHashMap() {super();accessOrder = false;}//传入的参数是一个Map的集合,调用父类的构造方法,取得键值对的顺序是插入顺序
public LinkedHashMap(Map<? extends K, ? extends V> m) {super(m);accessOrder = false;}//传入的参数为初始容量,加载因子,有序性标识(键值对保持顺序),调用了父类的构造方法
public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) {super(initialCapacity, loadFactor);this.accessOrder = accessOrder;}

LinkedHashMap的init()方法

由 LinkedHashMap的五种构造方法可知

  • 无论采用何种方式创建LinkedHashMap,其都会调用HashMap相应的构造函数
  • 不管调用HashMap的哪个构造函数,HashMap的构造函数都会在最后调用一个init()方法进行初始化
  • init()方法在HashMap中是一个空实现,而在LinkedHashMap中重写了它,用于初始化它所维护的双向链表
//Hashmap
/*** Constructs an empty <tt>HashMap</tt> with the default initial capacity* (16) and the default load factor (0.75).*/
public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR;threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);table = new Entry[DEFAULT_INITIAL_CAPACITY];init();
}//LinkedHashmap
@Overridevoid init() {//header初始化//hash为-1,其他的参数均为null//也就是说这个header不在数组中//只是用来标志开始元素和标志结束元素的header = new Entry<>(-1, null, null, null);header.before = header.after = header;}

LinkedHashMap基本数据结构(Entry:具体结构图在定义中)

  • LinkedHashMap中的Entry增加了两个指针 before 和 after,用于维护双向链接列表
    • before、after用于维护Entry插入的先后顺序
    • next用于维护HashMap各个桶中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);}

LinkedHashMap(Map<? extends K, ? extends V> m)

  • 构造一个与指定Map具有相同映射的 LinkedHashMap,其初始容量不小于16 (具体依赖于指定Map的大小),负载因子是 0.75,是 Java Collection Framework 规范推荐提供的,源码如下
/*** Constructs an insertion-ordered <tt>LinkedHashMap</tt> instance with* the same mappings as the specified map.  The <tt>LinkedHashMap</tt>* instance is created with a default load factor (0.75) and an initial* capacity sufficient to hold the mappings in the specified map.** @param  m the map whose mappings are to be placed in this map* @throws NullPointerException if the specified map is null*/
public LinkedHashMap(Map<? extends K, ? extends V> m) {super(m);       // 调用HashMap对应的构造函数accessOrder = false;    // 迭代顺序的默认值
}

LinkedHashMap的快速存取

LinkedHashMap 的存储实现 : put(key, vlaue)

  • LinkedHashMap完全继承了HashMap的 put(Key,Value) 方法
public V put(K key, V value) {if (table == EMPTY_TABLE) {      //数组为null时inflateTable(threshold);     //给数组根据阈值分配内容空间}if (key == null)      //key为null时return putForNullKey(value);int hash = hash(key);  //通过key计算hashint 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;//使用的是LinkedHashMap重写的方法e.recordAccess(this);         return oldValue;}}modCount++;//addEntry调用的是LinkedHashMap重写了的方法addEntry(hash, key, value, i);     return null;}
  • 只是对put(Key,Value)方法所调用的recordAccess方法和addEntry方法进行了重写
  • addEntry方法中还调用了removeEldestEntry方法,该方法是用来被重写的,一般如果用LinkedHashmap实现LRU算法,就要重写该方法
  • 比如可以将该方法覆写为如果设定的内存已满,则返回true,这样当再次向LinkedHashMap中putEntry时,在调用的addEntry方法中便会将近期最少使用的节点删除掉(header后的那个节点)
void recordAccess(HashMap<K,V> m) {      //将传入的HashMap类型的m强制转换成LinkedHashMap类型的    LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;  //accessOrder默认的是false,当accessOrder为true时进入if (lm.accessOrder) { lm.modCount++;//移除当前节点 remove(); 3      addBefore(lm.header);           }}  void addEntry(int hash, K key, V value, int bucketIndex) {           // 重写了HashMap中的createEntry方法createEntry(hash, key, value, bucketIndex);  // Remove eldest entry if instructedEntry<K,V> eldest = header.after;   //还是header自身if (removeEldestEntry(eldest)) {            removeEntryForKey(eldest.key);} else {  //扩容到原来的2倍  if (size >= threshold)             resize(2 * table.length);  }  
}protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {    return false;
}
  • 在LinkedHashMap的addEntry方法中,它重写了HashMap中的createEntry方法
 
void createEntry(int hash, K key, V value, int bucketIndex) { // 向哈希表中插入Entry,这点与HashMap中相同 //创建新的Entry并将其链入到数组对应桶的链表的头结点处, HashMap.Entry<K,V> old = table[bucketIndex];  Entry<K,V> e = new Entry<K,V>(hash, key, value, old);  table[bucketIndex] = e;     //在每次向哈希表插入Entry的同时,都会将其插入到双向链表的尾部,  //这样就按照Entry插入LinkedHashMap的先后顺序来迭代元素//(LinkedHashMap根据双向链表重写了迭代器)//同时,新put进来的Entry是最近访问的Entry,把其放在链表末尾 ,也符合LRU算法的实现  e.addBefore(header);  size++;  
}  
  • 在LinkedHashMap中向哈希表中插入新Entry的同时,还会通过Entry的addBefore方法将其链入到双向链表中
  • addBefore方法本质上是一个双向链表的插入操作
//插入有序不做处理,在访问有序做相应处理:addBefore(将当前节点插到header的前面)
private void addBefore(Entry<K,V> existingEntry) {             after  = existingEntry;     //existingEntry即为headerbefore = existingEntry.before;before.after = this;     //this即为要插入的节点after.before = this;
}
  • LinkedHashMap完全继承了HashMap的resize()方法,只是对它所调用的transfer方法进行了重写
  • Map扩容操作的核心在于重哈希
  • 重哈希是指重新计算原HashMap中的元素在新table数组中的位置并进行复制处理的过程,鉴于性能和LinkedHashMap自身特点的考量,LinkedHashMap对重哈希过程(transfer方法)进行了重写
void resize(int newCapacity) {       Entry[] oldTable = table;int oldCapacity = oldTable.length;// 若 oldCapacity 已达到最大值,直接将 threshold 设为 Integer.MAX_VALUEif (oldCapacity == MAXIMUM_CAPACITY) {  threshold = Integer.MAX_VALUE;return;             // 直接返回}// 否则,创建一个更大的数组Entry[] newTable = new Entry[newCapacity];//将每条Entry重新哈希到新的数组中transfer(newTable);  //LinkedHashMap对它所调用的transfer方法进行了重写table = newTable;threshold = (int)(newCapacity * loadFactor);  // 重新设定 threshold
}void transfer(HashMap.Entry[] newTable) {     int newCapacity = newTable.length;// 与HashMap相比,借助于双向链表的特点进行重哈希使得代码更加简洁for (Entry<K,V> e = header.after; e != header; e = e.after) {int index = indexFor(e.hash, newCapacity);   // 计算每个Entry所在的桶// 将其链入桶中的链表e.next = newTable[index];newTable[index] = e;   }
}

LinkedHashMap 的读取实现 :get(Object key)

public V get(Object key) {
//调用父类HashMap的getEntry()方法,取得要查找的元素Entry<K,V> e = (Entry<K,V>)getEntry(key);if (e == null)return null;
// 记录访问顺序e.recordAccess(this);return e.value;}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);}//在HashMap的put和get方法中,会调用该方法,在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 addBefore(Entry<K,V> existingEntry) {after  = existingEntry;before = existingEntry.before;before.after = this;after.before = this;}
  • recordAccess方法:
    • 如果链表中元素的排序规则是按照插入的先后顺序排序的话(accessOrder=false),该方法什么也不做
    • 如果链表中元素的排序规则是按照访问的先后顺序排序的话(accessOrder=true),则将e移到链表的末尾处
  • 调用LinkedHashMap的get(Object key)方法,返回值是 NULL,有如下两种可能:
    • 该 key 对应的值就是 null
    • HashMap 中不存在该 key

LinkeList与LRU(Least recently used,最近最少使用)算法

当accessOrder标志位为true时,表示双向链表中的元素按照访问的先后顺序排列,可以看到,虽然Entry插入链表的顺序依然是按照其put到LinkedHashMap中的顺序,但put和get方法均有调用recordAccess方法(put方法在key相同时会调用)

recordAccess方法判断accessOrder是否为true,如果是,则将当前访问的Entry(put进来的Entry或get出来的Entry)移到双向链表的尾部(key不相同时,put新Entry时,会调用addEntry,它会调用createEntry,该方法同样将新插入的元素放入到双向链表的尾部,既符合插入的先后顺序,又符合访问的先后顺序,因为这时该Entry也被访问了)

当标志位accessOrder的值为false时,表示双向链表中的元素按照Entry插入LinkedHashMap到中的先后顺序排序,即每次put到LinkedHashMap中的Entry都放在双向链表的尾部,这样遍历双向链表时,Entry的输出顺序便和插入的顺序一致,这也是默认的双向链表的存储顺序

当标志位accessOrder的值为false时,虽然也会调用recordAccess方法,但不做任何操作

使用LinkedList实现LRU算法

public class LRU<K,V> extends LinkedHashMap<K, V> implements Map<K, V>{private static final long serialVersionUID = 1L;public LRU(int initialCapacity,float loadFactor,boolean accessOrder) {super(initialCapacity, loadFactor, accessOrder);}/** * @description 重写LinkedHashMap中的removeEldestEntry方法,当LRU中元素多余6个时,*              删除最不经常使用的元素* @author rico       * @created 2017年5月12日 上午11:32:51      * @param eldest* @return     * @see java.util.LinkedHashMap#removeEldestEntry(java.util.Map.Entry)     */  @Overrideprotected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {// TODO Auto-generated method stubif(size() > 6){return true;}return false;}public static void main(String[] args) {LRU<Character, Integer> lru = new LRU<Character, Integer>(16, 0.75f, true);String s = "abcdefghijkl";for (int i = 0; i < s.length(); i++) {lru.put(s.charAt(i), i);}System.out.println("LRU中key为h的Entry的值为: " + lru.get('h'));System.out.println("LRU的大小 :" + lru.size());System.out.println("LRU :" + lru);}
}

LinkedList有序性原理分析

LinkedHashMap 增加了双向链表头结点header 和 标志位accessOrder两个属性用于保证迭代顺序。但是要想真正实现其有序性,还差临门一脚,那就是重写HashMap 的迭代器,其源码实现如下:

private abstract class LinkedHashIterator<T> implements Iterator<T> {Entry<K,V> nextEntry    = header.after;Entry<K,V> lastReturned = null;/*** The modCount value that the iterator believes that the backing* List should have.  If this expectation is violated, the iterator* has detected concurrent modification.*/int expectedModCount = modCount;public boolean hasNext() {         // 根据双向列表判断 return nextEntry != header;}public void remove() {if (lastReturned == null)throw new IllegalStateException();if (modCount != expectedModCount)throw new ConcurrentModificationException();LinkedHashMap.this.remove(lastReturned.key);lastReturned = null;expectedModCount = modCount;}Entry<K,V> nextEntry() {        // 迭代输出双向链表各节点if (modCount != expectedModCount)throw new ConcurrentModificationException();if (nextEntry == header)throw new NoSuchElementException();Entry<K,V> e = lastReturned = nextEntry;nextEntry = e.after;return e;}
}// Key 迭代器,KeySet
private class KeyIterator extends LinkedHashIterator<K> {   public K next() { return nextEntry().getKey(); }
}// Value 迭代器,Values(Collection)
private class ValueIterator extends LinkedHashIterator<V> {public V next() { return nextEntry().value; }
}// Entry 迭代器,EntrySet
private class EntryIterator extends LinkedHashIterator<Map.Entry<K,V>> {public Map.Entry<K,V> next() { return nextEntry(); }
}

总结

  • 上文是基于JDK1.6的实现,实际上JDK1.8对其进行了改动
  • linkedhashmap在hashmap的数组加链表结构的基础上,将所有节点连成了一个双向链表
  • 当主动传入的accessOrder参数为false时, 使用put方法时,新加入元素不仅加入哈希桶中,还被加入双向链表末尾,get方法使用时不会把元素放到双向链表尾部
  • 当主动传入的accessOrder参数为true时,使用put方法新加入的元素,如果遇到了哈希冲突,并且对key值相同的元素进行了替换,就会被放在双向链表的尾部,当元素超过上限且removeEldestEntry方法返回true时,直接删除最早元素以便新元素插入。如果没有冲突直接放入,同样加入到链表尾部。使用get方法时会把get到的元素放入双向链表尾部
  • inkedhashmap的扩容比hashmap来的方便,因为hashmap需要将原来的每个链表的元素分别在新数组进行反向插入链化,而linkedhashmap的元素都连在一个链表上,可以直接迭代然后插入
  • linkedhashmap的removeEldestEntry方法默认返回false,要实现LRU很重要的一点就是集合满时要将最久未访问的元素删除,在linkedhashmap中这个元素就是头指针指向的元素。实现LRU可以直接实现继承linkedhashmap并重写removeEldestEntry方法来设置缓存大小。jdk中实现了LRUCache也可以直接使用
  • 在put操作上,虽然LinkedHashMap完全继承了HashMap的put操作,但是在细节上还是做了一定的调整,比如,在LinkedHashMap中向哈希表中插入新Entry的同时,还会通过Entry的addBefore方法将其链入到双向链表中
  • 在扩容操作上,虽然LinkedHashMap完全继承了HashMap的resize操作,但是鉴于性能和LinkedHashMap自身特点的考量,LinkedHashMap对其中的重哈希过程(transfer方法)进行了重写
  • 在读取操作上,LinkedHashMap中重写了HashMap中的get方法(加入recordAccess方法,重写transfer方法),通过HashMap中的getEntry方法获取Entry对象

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/442159.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

过年饺子馅儿的做法和配料,记在备忘录随时查看

每逢过年&#xff0c;家家户户的餐桌上总少不了一盘热气腾腾的饺子。对于我们中国人来说&#xff0c;饺子不仅仅是一种美食&#xff0c;更承载了团圆的情感和美好的寓意。尤其是在这个阖家欢乐的时刻&#xff0c;能够亲手为家人包上一顿美味的饺子&#xff0c;无疑是一件既有意…

Swift Vapor 教程(项目创建)

The future of web development. 在初次接触 Swift Vapor 时&#xff0c;感觉代码比较清爽&#xff0c;用起来逻辑比较清晰。 困难点&#xff1a; Swift Vapor 使用了JWT管理三方库&#xff0c;比较吃网络Swift Vapor 搭建环境比较复杂初次使用Swift Vapor 尽量不要使用MySql。…

研学活动报名平台源码开发方案

一、项目背景与目标 &#xff08;一&#xff09;项目背景 研学活动报名平台旨在为活动组织者提供方便快捷的研学活动管理工具&#xff0c;同时为用户提供全面的活动搜索、报名和支付等功能。通过该系统&#xff0c;活动组织者能够更好地管理活动报名信息&#xff0c;用户也可…

ASP.NET Core 使用 SignalR 的简单示例

写在前面 ASP.NET SignalR 是一个开源代码库&#xff0c;简化了Web实时通讯方案&#xff0c;可以实时地通过服务端将信息同步推送到各个客户端&#xff0c;可应用于 需要从服务器进行高频更新的应用&#xff1a;包括游戏、社交网络、投票、拍卖、地图和GPS应用&#xff1b; 仪…

管理的四种风格

前言 管理的四种风格,一般的领导大概就是这几种管理模式,告知,辅导,参与,授权,还有就是乱搞式(神经病模式)。 一、告知式 告知式是指组织通过正式、明确的渠道,将信息传达给员工。这种方式通常用于传递基本的规章制度、工作流程、政策文件等。告知式的作用在于确保员…

以“美”为鉴,探寻香港比特币现货ETF的未来发展

出品&#xff5c;欧科云链研究院 作者&#xff5c;Hedy Bi 根据The Block于1月29日的报道&#xff0c;嘉实国际成为了首家向香港证监会提交比特币现货ETF申请的机构。早在去年12月22日&#xff0c;香港证监会发布了《有关证监会认可基金投资虚拟资产的通函》&#xff0c;明确…

anaconda离线安装包的方法

当设备没有网络时&#xff0c;可以使用有网络的设备先下载所需安装包&#xff0c;然后离线拷贝到需要安装的设备&#xff0c;最后安装。 一. 下载所需安装包 下载命令&#xff1a;使用pip download。详细描述参见pip download -h 以"blind-watermark"为例。 pip …

Android 跳转应用设置/热点界面或等常用操作

Android 跳转应用设置/热点界面或等常用操作 https://www.jianshu.com/p/ba7164126690 android学习进阶——Setting https://blog.csdn.net/csdn_wanziooo/article/details/81980984 Android 7.1 以太网反射 EthernetManager 配置 DHCP、静态 IP https://codeleading.com/art…

云原生 k8s 可能使用到的端口整理【不定期更新】

k8s 因为涉及到的组件太多了&#xff0c;所以端口有很多&#xff0c;这里整理了日常所接触的接口&#xff0c;后续有新的再更新。 如果是通过公网 IP 进行安装的时候需要根据实际情况有选择的进行放开&#xff1b;一般只有云厂商会提供公网 IP 访问&#xff0c;自建的话不建议 …

uniapp使用u-popup组件弹窗出现页面还可滑动

*1、问题所在&#xff1a; 弹窗遮罩层出现了页面依旧可以上下滑动 2、要求: 为了用户更好交互体验&#xff0c;弹窗出现后应禁止页面往下滑动 3、实现思路&#xff1a; 在弹窗盒子外层添加个阻止触摸冒泡事件&#xff0c;使用touchmove.stop.prevent 4、代码如下&#xff…

Linux下如何编译C/C++代码?从.c到.exe经历了什么?

&#x1f388;个人主页:&#x1f388; :✨✨✨初阶牛✨✨✨ &#x1f43b;强烈推荐优质专栏: &#x1f354;&#x1f35f;&#x1f32f;C的世界(持续更新中) &#x1f43b;推荐专栏1: &#x1f354;&#x1f35f;&#x1f32f;C语言初阶 &#x1f43b;推荐专栏2: &#x1f354;…

事务、MVCC、锁

目录 事务MVCC锁 事务 四大特性&#xff1a;ACID 脏读&#xff1a;事务A读取到未提交事务B修改的数据 不可重复读&#xff1a;事务A修改了未提交事务B读取的数据 幻读&#xff1a;事务A增删了未提交事务B读取的数据 不可重复读与幻读都是读取的结果不同&#xff0c;前者侧重于…