Java集合系列之LinkedHashMap

本文详细介绍了Java集合类LinkedHashMap的工作原理及其实现方式,包括与HashMap的区别、底层数据结构、维护双向链表的方法以及如何利用LinkedHashMap实现LRU缓存。

Java集合系列之LinkedHashMap

Hello,大家好,前面给大家讲了HashMap,LinkedList,知道了HashMap为数组+单向链表,LinkedList为双向链表实现的。今天给大家介绍一个(HashMap+"LinkedList")的集合,LinkedHashMap,其中HashMap用于存储数据,"LinkedList"用于存储数据顺序。OK,废话少说,老套路,文章结构:

  1. LinkedHashMap和HashMap区别
  2. LinkedHashMap底层实现
  3. 利用LinkedHashMap实现LRU缓存

1. LinkedHashMap和HashMap区别

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

public static void main(String[] args) {
    Map<String, String> map = new LinkedHashMap<String, String>();
    map.put("apple", "苹果");
    map.put("watermelon", "西瓜");
    map.put("banana", "香蕉");
    map.put("peach", "桃子");

    Iterator iter = map.entrySet().iterator();
    while (iter.hasNext()) {
        Map.Entry entry = (Map.Entry) iter.next();
        System.out.println(entry.getKey() + "=" + entry.getValue());
    }
}
复制代码
输出为:
apple=苹果
watermelon=西瓜
banana=香蕉
peach=桃子
复制代码

可以看到,在使用上,LinkedHashMap和HashMap的区别就是LinkedHashMap是有序的。 上面这个例子是根据插入顺序排序,此外,LinkedHashMap还有一个参数决定是否在此基础上再根据访问顺序(get,put)排序,记住,是在插入顺序的基础上再排序,后面看了源码就知道为什么了。看下例子:

public static void main(String[] args) {
    Map<String, String> map = new LinkedHashMap<String, String>(16,0.75f,true);
    map.put("apple", "苹果");
    map.put("watermelon", "西瓜");
    map.put("banana", "香蕉");
    map.put("peach", "桃子");

    map.get("banana");
    map.get("apple");

    Iterator iter = map.entrySet().iterator();
    while (iter.hasNext()) {
        Map.Entry entry = (Map.Entry) iter.next();
        System.out.println(entry.getKey() + "=" + entry.getValue());
    }
}
复制代码
输出为:
watermelon=西瓜
peach=桃子
banana=香蕉
apple=苹果
复制代码

可以看到香蕉和苹果在原来排序的基础上又排后了。

2. LinkedHashMap底层实现

我先说结论,然后再慢慢跟代码。

  • LinkedHashMap继承自HashMap,它的新增(put)和获取(get)方法都是复用父类的HashMap的代码,只是自己重写了put给get内部的某些接口来搞事情,这个特性在C++中叫钩子技术,在Java里面大家喜欢叫多态,其实多态这个词并不能很好的形容这种现象。
  • LinkedHashMap的数据存储和HashMap的结构一样采用(数组+单向链表)的形式,只是在每次节点Entry中增加了用于维护顺序的before和after变量维护了一个双向链表来保存LinkedHashMap的存储顺序,当调用迭代器的时候不再使用HashMap的的迭代器,而是自己写迭代器来遍历这个双向链表即可。
  • HashMap和LinkedHashMap内部逻辑图如下:

好了,大家肯定会觉得很神奇,如图所示,本来HashMap的数据是0-7这样的无须的,而LinkedHashMap却把它变成了如图所示的1.6.5.3.。。2这样的有顺序了。到底是如何做到的了?其实说白了,就一句话,钩子技术,在put和get的时候维护好了这个双向链表,遍历的时候就有序了。好了,一步一步的跟。 先看一下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);
    }
    ...
}
复制代码

可以看到继承自HashMap的Entry,并且多了两个指针before和after,这两个指针说白了,就是为了维护双向链表新加的两个指针。 列一下新Entry的所有成员变量吧:

  • K key
  • V value
  • Entry<K, V> next
  • int hash
  • Entry<K, V> before
  • Entry<K, V> after

其中前面四个,是从HashMap.Entry中继承过来的;后面两个,是是LinkedHashMap独有的。不要搞错了next和before、After,next是用于维护HashMap指定table位置上连接的Entry的顺序的,before、After是用于维护Entry插入的先后顺序的(为了维护双向链表)。

2.1 初始化
 1 public LinkedHashMap() {
 2 super();
 3     accessOrder = false;
 4 }
复制代码
1 public HashMap() {
2     this.loadFactor = DEFAULT_LOAD_FACTOR;
3     threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
4     table = new Entry[DEFAULT_INITIAL_CAPACITY];
5     init();
6 }
复制代码
 1 void init() {
 2     header = new Entry<K,V>(-1, null, null, null);
 3     header.before = header.after = header;
 4 }
复制代码

这里出现了第一个钩子技术,尽管init()方法定义在HashMap中,但是由于LinkedHashMap重写了init方法,所以根据多态的语法,会调用LinkedHashMap的init方法,该方法初始化了一个Header,这个Header就是双向链表的链表头..

2.2 LinkedHashMap添加元素

HashMap中的put方法:

 1 public V put(K key, V value) {
 2     if (key == null)
 3         return putForNullKey(value);
 4     int hash = hash(key.hashCode());
 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 
16     modCount++;
17     addEntry(hash, key, value, i);
18     return null;
19 }
复制代码

LinkedHashMap中的addEntry(又是一个钩子技术):

 1 void addEntry(int hash, K key, V value, int bucketIndex) {
 2     createEntry(hash, key, value, bucketIndex);
 3 
 4     // Remove eldest entry if instructed, else grow capacity if appropriate
 5     Entry<K,V> eldest = header.after;
 6     if (removeEldestEntry(eldest)) {
 7         removeEntryForKey(eldest.key);
 8     } else {
 9         if (size >= threshold)
10             resize(2 * table.length);
11     }
12 }
复制代码
1 void createEntry(int hash, K key, V value, int bucketIndex) {
2     HashMap.Entry<K,V> old = table[bucketIndex];
3     Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
4     table[bucketIndex] = e;
5     e.addBefore(header);
6     size++;
7 }
复制代码
private void addBefore(Entry<K,V> existingEntry) {
    after  = existingEntry;
    before = existingEntry.before;
    before.after = this;
    after.before = this;
}
复制代码

好了,addEntry先把数据加到HashMap中的结构中(数组+单向链表),然后调用addBefore,这个我就不和大家画图了,其实就是挪动自己和Header的Before与After成员变量指针把自己加到双向链表的尾巴上。 同样的,无论put多少次,都会把当前元素加到队列尾巴上。这下大家知道怎么维护这个双向队列的了吧。

上面说了LinkedHashMap在新增数据的时候自动维护了双向列表,这要还要提一下的是LinkedHashMap的另外一个属性,根据查询顺序排序,说白了,就是在get的时候或者put(更新时)把元素丢到双向队列的尾巴上。这样不就排序了吗?这里涉及到LinkedHashMap的另外一个构造方法:

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

第三个参数,accessOrder为是否开启查询排序功能的开关,默认为False。如果想开启那么必须调用这个构造方法。 然后看下get和put(更新操作)时是如何维护这个队列的。

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的时候,代码11行(见上面的代码),也是调用了e.recordAccess(this);我们来看下这个方法:

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的情况下。 假设现在我们开启了accessOrder,然后调用get("111");看下是如何操作的:

3. 利用LinkedHashMap实现LRU缓存

LRU即Least Recently Used,最近最少使用,也就是说,当缓存满了,会优先淘汰那些最近最不常访问的数据。我们的LinkedHashMap正好满足这个特性,为什么呢?当我们开启accessOrder为true时,最新访问(get或者put(更新操作))的数据会被丢到队列的尾巴处,那么双向队列的头就是最不经常使用的数据了。比如:

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

此外,LinkedHashMap还提供了一个方法,这个方法就是为了我们实现LRU缓存而提供的,removeEldestEntry(Map.Entry<K,V> eldest) 方法。该方法可以提供在每次添加新条目时移除最旧条目的实现程序,默认返回 false

来,给大家一个简陋的LRU缓存:

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

    protected boolean removeEldestEntry(java.util.Map.Entry eldest)
    {
        //逻辑很简单,当大小超出了Map的容量,就移除掉双向队列头部的元素,给其他元素腾出点地来。
        return size() > maxElements;
    }

    private static final long serialVersionUID = 1L;
    protected int maxElements;
}
复制代码

是不是很简单。。

结语

其实 LinkedHashMap 几乎和 HashMap 一样:从技术上来说,不同的是它定义了一个 Entry<K,V> header,这个 header 不是放在 Table 里,它是额外独立出来的。LinkedHashMap 通过继承 hashMap 中的 Entry<K,V>,并添加两个属性 Entry<K,V> before,after,和 header 结合起来组成一个双向链表,来实现按插入顺序或访问顺序排序。如何维护这个双向链表了,就是在get和put的时候用了钩子技术(多态)调用LinkedHashMap重写的方法来维护这个双向链表,然后迭代的时候直接迭代这个双向链表即可,好了LinkedHashMap算是给大家分享完了,Over,Have a good day .

<think>嗯,用户让我介绍一下Java集合中的LinkedHashMap,还要结合源码分析。首先,我得回忆一下LinkedHashMap的基本概念和它在Java集合框架中的位置。LinkedHashMap是HashMap的子类,所以它应该具有HashMap的所有特性,比如允许null键值,非同步等等。但它的特殊之处在于维护了一个双向链表,用来保持插入顺序或者访问顺序。这可能就是用户想深入了解的地方。 接下来,我需要考虑用户的需求。他们可能已经对HashMap有一定的了解,现在想进一步学习LinkedHashMap的工作原理和内部实现。所以,我应该先对比HashMap和LinkedHashMap的区别,强调LinkedHashMap的有序性。然后,可能需要讲解LinkedHashMap数据结构,比如Entry节点的结构,双向链表是如何维护的。 然后,源码分析部分需要深入LinkedHashMap的关键方法,比如put、get、remove,以及如何维护访问顺序。比如,在HashMap的put方法中,LinkedHashMap可能重写了某些方法,比如afterNodeAccess和afterNodeInsertion,这些钩子方法的作用是什么。另外,访问顺序模式下,最近访问的节点会被移动到链表末尾,这在实现LRU缓存时很有用,这个点可能也需要提到。 用户可能还想知道LinkedHashMap如何实现迭代顺序的有序性。这时候需要分析迭代器的实现,可能是通过遍历双向链表而不是像HashMap那样遍历桶数。此外,构造方法中的accessOrder参数的作用,以及如何影响元素顺序,这也是关键点。 另外,要注意到LinkedHashMap在实现LRU缓存时的应用。当accessOrder设置为true时,每次访问元素都会调整顺序,当达到容量时,会移除最老的元素。这部分可能需要结合removeEldestEntry方法的源码,说明如何扩展这个方法来控制缓存策略。 在结构上,应该先介绍LinkedHashMap的基本概念,然后数据结构,接着关键源码分析,最后应用场景。可能还需要给出一个简单的例子,比如如何用LinkedHashMap实现LRU缓存,这样用户更容易理解。 需要确认是否覆盖了主要的方法和内部类,比如Entry的结构,是否有新增的字段如before和after。同时,要注意源码的版本是否正确,假设是Java 8的源码,因为不同版本可能有差异。例如,在Java 8中,HashMap引入了树化结构,但LinkedHashMap仍然使用双向链表,这部分是否需要提及? 在讲解源码时,可能需要用代码块展示关键部分,比如Entry类的定义,以及afterNodeAccess方法的具体实现。但要注意不要粘贴过多代码,而是选择关键部分进行解释。 另外,用户可能对LinkedHashMap的性能感兴趣,比如HashMap相比,插入、删除、访问的时间复杂度是否有变化。虽然LinkedHashMap维护了双向链表,但大多数操作的时间复杂度仍然是O(1),因为链表操作是常量时间的。 最后,总结LinkedHashMap的特点和适用场景,帮助用户理解何时应该选择LinkedHashMap而不是其他集合类。例如,需要保持插入顺序或访问顺序,或者需要实现简单的LRU缓存时。 需要确保解释清晰,逻辑连贯,避免技术术语过多导致理解困难。同时,结合源码分析时要抓住重点,展示关键代码部分并解释其作用,帮助用户理解内部机制。</think>### Java 集合LinkedHashMap 解析源码分析 `LinkedHashMap` 是 `HashMap` 的子类,在保留 `HashMap` 高效查询能力的基础上,**通过双向链表维护元素的插入顺序或访问顺序**。它是实现 `LRU` 缓存策略的理想数据结构。 -- #### 核心特性 1. **有序性**:默认按插入顺序迭代,可配置为按访问顺序(最近访问的元素排在末尾)。 2. **性能**:查询、插入、删除的时间复杂度为 `O(1)`,略慢于 `HashMap`(需维护链表)。 3. **继承结构**:继承 `HashMap`,复用哈希表逻辑,重写链表维护相关方法。 --- ### 数据结构关键源码 #### 1. Entry 结构扩展 `LinkedHashMap.Entry` 继承 `HashMap.Node`,**新增 `before` 和 `after` 指针**,形成双向链表。 ```java 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); } } ``` -- #### 2. 关键属性 - **head/tail**:维护链表的头尾节点。 - **accessOrder**:`true` 表示按访问顺序排序,`false`(默认)表示按插入顺序。 ```java transient LinkedHashMap.Entry<K,V> head; // 链表头(最老元素) transient LinkedHashMap.Entry<K,V> tail; // 链表尾(最新元素) final boolean accessOrder; // 顺序控制标志 ``` --- ### 核心方法源码分析 #### 1. 插入链表维护 `LinkedHashMap` **未重写 `put()`**,直接复用 `HashMap` 的逻辑,但通过重写以下钩子方法维护链表: ```java // 插入新节点后,维护链表结构(可能触发删除最老元素) void afterNodeInsertion(boolean evict) { LinkedHashMap.Entry<K,V> first; if (evict && (first = head) != null && removeEldestEntry(first)) { removeNode(first.key, null, false, true); // 删除头节点(LRU 逻辑) } } // 是否删除最老元素(默认返回 false,需子类重写实现 LRU) protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { return false; } ``` --- #### 2. 访问顺序调整 当 `accessOrder=true` 时,调用 `get()` 或 `getOrDefault()` 会触发 `afterNodeAccess()`,将节点移到链表末尾。 ```java public V get(Object key) { Node<K,V> e; if ((e = getNode(key)) == null) return null; if (accessOrder) afterNodeAccess(e); // 调整链表顺序 return e.value; } // 将节点移动到链表尾部(最近访问) void afterNodeAccess(Node<K,V> e) { LinkedHashMap.Entry<K,V> last = tail; if (accessOrder && last != e) { LinkedHashMap.Entry<K,V> p = (Entry<K,V>)e; LinkedHashMap.Entry<K,V> 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++; } } ``` --- #### 3. 迭代器实现 `LinkedHashMap` 的迭代器直接遍历双向链表,而非哈希桶数,**保证顺序性**。 ```java abstract class LinkedHashIterator { LinkedHashMap.Entry<K,V> next = head; // 从头节点开始 LinkedHashMap.Entry<K,V> current; // ... public final boolean hasNext() { return next != null; } final LinkedHashMap.Entry<K,V> nextNode() { LinkedHashMap.Entry<K,V> e = next; // ... current = e; next = e.after; // 通过 after 指针遍历 return e; } } ``` -- ### 应用场景:实现 LRU 缓存 通过重写 `removeEldestEntry()`,限制最大容量并删除最久未使用的元素。 ```java public class LRUCache<K, V> extends LinkedHashMap<K, V> { private final int maxCapacity; public LRUCache(int maxCapacity) { super(maxCapacity, 0.75f, true); // accessOrder=true this.maxCapacity = maxCapacity; } @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > maxCapacity; // 容量超限时删除最老元素 } } ``` --- ### 总结 - **有序性**:双向链表维护插入/访问顺序。 - **扩展性**:通过钩子方法(如 `afterNodeAccess`)实现行为定制。 - **高效性**:在 `HashMap` 基础上以少量性能代价换取顺序控制。 - **应用场景**:需要顺序迭代或实现类似 LRU 的缓存策略时优先选择。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值