一文详解LinkedHashMap

587 阅读4分钟

我们工作中最常用到的是HashMap,几乎没有用到过LinkedHashMap,LinkedHashMap到底有什么用呢?

用处非常大,如果你被面试到过手写LRU,就会感叹为啥没有早点看到这篇文章。

1. 概述

LinkedHashMap继承自 HashMap,因此具有 HashMap的所有特性。

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

LinkedHashMap内部维护了一个双向链表,用来记录插入顺序或者访问顺序。

// 双链表头结点
transient LinkedHashMap.Entry<K,V> head;

// 双链表尾节点
transient LinkedHashMap.Entry<K,V> tail;

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. 初始化

public LinkedHashMap() {
  	// 调用的是父类HashMap的构造方法
    super();
  	// 默认是false,表示按照插入顺序排序,最近插入的移动到链表尾部
  	// true表示链表按照访问顺序排序,最近访问的移动到链表尾部
    accessOrder = false;
}

// 初始化时,可以指定排序方式accessOrder
public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

3. 心机的HashMap

在HashMap里面有几个空方法,留给子类去实现。

通过名称就能知道这几个方法是干嘛用的,可见好的命名习惯有多么重要,连注释都省了。

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

  // 当一个节点被访问后,做的操作
	void afterNodeAccess(Node<K,V> p) { }
  
  // 当新插入一个节点时,做的操作
	void afterNodeInsertion(boolean evict) { }
  
  // 当删除一个节点时,做的操作
	void afterNodeRemoval(Node<K,V> p) { }

}

这几个空方法,在HashMap里面也被调用了,虽然没有任何操作。当子类LinkedHashMap实现了这些方法,就能起到应有的作用了。

【HashMap调用空方法.png】

4. 实现父类HashMap的空方法

看一下LinkedHashMap是实现这些方法的:

4.1 当一个节点被访问后,移动到链表末尾

void afterNodeAccess(Node<K,V> e) {
    LinkedHashMap.Entry<K,V> last;
  	// 如果设置了按照访问顺序排序,并且当前节点不是尾节点,才进行操作
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
      	// 如果当前节点是头节点,就把当前节点的下个节点设置成头节点
        if (b == null)
            head = a;
        else
          	// 否则,就断开跟前一个节点的连接
            b.after = a;
      	
      	// 当前节点是尾节点,就把当前节点的前一个节点设置成尾节点
        if (a == null)
          	last = b;
        else
          	// 否则,就断开跟下一个节点的连接
            a.before = b;
      
      	// 如果只有一个节点,就把当前节点设置成头节点
        if (last == null)
            head = p;
        else {
          	// 否则,就把当前节点追加到尾节点上
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}

4. 2 当一个节点被新插入,删除最旧的节点

void afterNodeInsertion(boolean evict) {
    LinkedHashMap.Entry<K,V> first;
  	// 如果头节点不是null,并且设置了删除最旧节点,就会删除头节点
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

// 是否删除最旧节点,默认是false,初始化LinkedHashMap可以手动设置
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

4.3 删除一个节点

// 删除节点e
void afterNodeRemoval(Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    p.before = p.after = null;
  	// 如果当前节点是头节点,就把当前节点的下个节点设置成头节点
    if (b == null)
        head = a;
    else
      	// 否则,就断开跟前一个节点的连接
        b.after = a;
  
  	// 当前节点是尾节点,就把当前节点的前一个节点设置成尾节点
    if (a == null)
        tail = b;
    else
      	// 否则,就断开跟下一个节点的连接
        a.before = b;
}

5. put方法源码

LinkedHashMap其实是调用的父类HashMap的put的方法,只是覆盖了HashMap新建节点的方法

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
  	// 新建节点
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
  	// 新节点追加到链表尾部
    linkNodeLast(p);
    return p;
}

private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}

6. 实现LRU缓存

由于LinkedHashMap记录了节点插入顺序和访问顺序,新节点或者经常访问的节点被移动末尾,超过固定长度的时候,就删除头节点,这些功能就是为了实现LRU(最近最少访问)缓存量身定做的。

class LRUCache<K, V> extends LinkedHashMap<K, V> {
  	// 最大长度是3
    private static final int MAX_SIZE = 3;

  	// 超过最大长度,就删除最旧的节点
  	@Override
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return size() > MAX_SIZE;
    }

    LRUCache() {
      	// true表示按访问顺序排序
        super(MAX_SIZE, 0.75f, true);
    }
}

测试一下:

public static void main(String[] args) {
    LRUCache<Integer, String> cache = new LRUCache<>();
    cache.put(1, "a");
    cache.put(2, "b");
    cache.put(3, "c");
  	System.out.println(cache.keySet()); // 输出 [1, 2, 3]
    cache.get(2);
  	// 最近访问的,被移动到末尾
    System.out.println(cache.keySet()); // 输出 [1, 3, 2]
  
  	lru.put(4, "d");
  	// 超过最大长度,就删除头节点
  	System.out.println(lru.keySet()); // 输出 [3, 2, 4]
}

7. 总结

LinkedHashMap相比HashMap,就多了两个功能:

  1. 用双链表记录了插入顺序和访问顺序
  2. 超过最大长度,删除链表头节点

在日常开发中,虽然几乎不会用到LinkedHashMap,但是当你手动去实现一个LRU的缓存的时候,必定会用到LinkedHashMap。就算你不用,也一定会参考LinkedHashMap的设计。

面试的时候,也经常遇到面试官让你手写一个LRU,看完了LinkedHashMap源码,再也不用担心不会写了。