JAVA LinkedHashMap与LRU,LinkedHashMap keySet遍历Map失败之谜

本文探讨了Java中LinkedHashMap如何实现LRU缓存策略,并详细解释了为何通过keySet遍历LinkedHashMap时会抛出`ConcurrentModificationException`。通过分析源码,揭示了迭代过程中get操作导致的modCount变化是引发异常的关键。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近看图片缓存的时候看到了LinkedHashMap,继承于HashMap(差别百度一大堆),于是仔细看了一下,这里先介绍LinkedHashMap怎么实现LRU的,在介绍使用keySet来遍历LinkedHashMap(以下简称“LHM”)为什么会出现java.util.ConcurrentModificationException错误。
1.LRU(Least Recently Used 近期最少使用)的实现,在LHM的文档里面有一句话“This kind of map is well-suited to building LRU caches”,那么它是如何维护LRU的序列的呢?首先注意到它有一个构造方法

public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder)

这里第三个参数是关键,如果为true表明排列方式为访问顺序,false表明为插入顺序。客户端将其设为true就能实现LRU了,但LHM具体是怎么来实现的呢?这里的实现肯定与get有关,于是看看get,下面列出了HashMap.get与LHM.get的源码:

    public V get(Object key) {//LHM的get
        Entry<K,V> e = (Entry<K,V>)getEntry(key);
        if (e == null)
            return null;
        e.recordAccess(this);
        return e.value;
    }
     public V get(Object key) {//HashMap的get
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
    }

这里可以注意到LRU的关键实现就是recordAccess这个方法。源码为:

 void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            if (lm.accessOrder) {
                lm.modCount++;
                remove();
                addBefore(lm.header);
            }
        }

里面有两个关键方法remove和addBefore,从名字可以看出移除这个元素并添加到尾部。LHM的entry是一个双向链表,这也就是为什么LHM为什么可以保持put进去的顺序,这里大家可以想象这两个方法如何实现的,这里就不贴代码了,注意在remove方法前面的modCount++,这句话是造就后面异常的关键。
2.LHM遍历出现异常。(前提是构造方法第三个参数为true)
我这里用了两个方法遍历,一个是用entrySet(成功),一个是用keySet(失败,出现ConcurrentModificationException错误),显然这两个方法是在HashMap是可以成功遍历的。这里贴一下代码和异常,方便大家分析:

LinkedHashMap<String, String> map=new LinkedHashMap<>(10, 0.75f, true);
        map.put("1", "1");
        map.put("2", "2");
        map.put("3", "3");
        Iterator<String> it = map.keySet().iterator();
        System.out.println(map.keySet().getClass()+" "+it.getClass());
        while ( it.hasNext()) {
            System.out.println(map.get(it.next()));
        }
//输出
class java.util.HashMap$KeySet class java.util.LinkedHashMap$KeyIterator
Exception in thread "main" 1
java.util.ConcurrentModificationException
    at java.util.LinkedHashMap$LinkedHashIterator.nextEntry(Unknown Source)
    at java.util.LinkedHashMap$KeyIterator.next(Unknown Source)
    at access.Main.main(Main.java:19)

根据异常情况可看到在LinkedHashIterator.nextEntry里面出现了异常,这方法里面有个判断:
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
这个expectedModCount初始化为modCount,那么这个异常是如何抛出的呢?大家应该能想到我上面说的get里面有个modCount++,这遍历时调用了get,导致两者不等,于是抛出了异常。现在还有一个问题,这个nextEntry是如何触发的呢?它的上一层是LinkedHashMap$KeyIterator.next,这里请大家结合代码看看输出的第一行,可以得出LMH在keySet方法后得到了HashMap的keySet对象,这keySet方法是LHM继承HashMap的,没有覆盖,得到HashMap的keySet不奇怪,后面再调用iterator方法从HashMap的keySet得到了LinkedHashMap的KeyIterator就用点奇怪了,原因是HashMap的的keySet里面的iterator方法调用的HashMap的newKeyIterator方法,而这个方法被LHM覆盖了,于是返回LHM的KeyIterator,最后在调用get方法的时候就抛出了异常。这个设计真巧妙!!!
最后说一下这个ConcurrentModificationException,大家可以去百度,这里贴部分文档信息“This exception may be thrown by methods that have detected concurrent modification of an object when such modification is not permissible.
For example, it is not generally permissible for one thread to modify a Collection while another thread is iterating over it.”大概意思检查到了修改但修改是不允许的,如一个线程正在迭代一个集合,另一个线程通常是不允许去改变这个集合的。

<think>根据用户的问题,他需要JavaLinkedHashMap遍历的示例代码。从引用中可以看到,提供了三种遍历方法:1.使用entrySet的迭代器遍历(引用[1])2.使用ListIterator进行逆序遍历(引用[2])3.插入顺序遍历(引用[3])我将提供三种遍历方式的示例代码,并解释每种方式的特点。</think>### Java LinkedHashMap遍历方法及示例 `LinkedHashMap`在`HashMap`基础上维护了键值对的插入顺序或访问顺序(取决于构造参数),遍历时会按顺序输出。以下是三种常用遍历方式: #### 1. 顺序遍历(插入顺序/访问顺序) ```java import java.util.LinkedHashMap; import java.util.Map; public class LinkedHashMapTraversal { public static void main(String[] args) { // 创建LinkedHashMap (true表示按访问顺序排序) LinkedHashMap<String, Integer> map = new LinkedHashMap<>(16, 0.75f, true); map.put("Apple", 10); map.put("Banana", 20); map.put("Cherry", 30); // 方法1:增强for循环遍历entrySet System.out.println("=== 方法1: entrySet遍历 ==="); for (Map.Entry<String, Integer> entry : map.entrySet()) { System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue()); } // 方法2:forEach遍历 (Java 8+) System.out.println("\n=== 方法2: forEach遍历 ==="); map.forEach((k, v) -> System.out.println("Key: " + k + ", Value: " + v)); } } ``` **输出**: ``` === 方法1: entrySet遍历 === Key: Apple, Value: 10 Key: Banana, Value: 20 Key: Cherry, Value: 30 === 方法2: forEach遍历 === Key: Apple, Value: 10 Key: Banana, Value: 20 Key: Cherry, Value: 30 ``` #### 2. 逆序遍历(从尾到头) ```java import java.util.*; import java.util.Map.Entry; public class ReverseTraversal { public static void main(String[] args) { LinkedHashMap<String, Integer> map = new LinkedHashMap<>(); map.put("A", 1); map.put("B", 2); map.put("C", 3); // 转换为ArrayList后用ListIterator逆序遍历 System.out.println("=== 逆序遍历 ==="); List<Entry<String, Integer>> entryList = new ArrayList<>(map.entrySet()); ListIterator<Entry<String, Integer>> iterator = entryList.listIterator(entryList.size()); // 从尾部开始 while (iterator.hasPrevious()) { Entry<String, Integer> entry = iterator.previous(); System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue()); } } } ``` **输出**: ``` === 逆序遍历 === Key: C, Value: 3 Key: B, Value: 2 Key: A, Value: 1 ``` #### 3. 单独遍历键或值 ```java // 遍历所有键 System.out.println("\n=== 遍历Keys ==="); for (String key : map.keySet()) { System.out.println("Key: " + key); } // 遍历所有值 System.out.println("\n=== 遍历Values ==="); for (Integer value : map.values()) { System.out.println("Value: " + value); } ``` ### 关键特性说明 1. **顺序保证**: - 默认按插入顺序遍历(构造参数`accessOrder=false`) - 设置`accessOrder=true`时按访问顺序排序(最近访问的元素移到末尾)[^3] 2. **性能**: - 遍历时间复杂度 $O(n)$ - 相比`HashMap`多维护双向链表,空间占用略高 3. **迭代器安全**: - 使用迭代器时修改集合会抛`ConcurrentModificationException` - 删除元素需通过迭代器的`remove()`方法 > **应用场景**:LRU缓存实现、需要保持插入顺序的日志记录、配置项读取等[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值