LinkedHashMap解析

1,075 阅读4分钟
知乎:zhuanlan.zhihu.com/p/77152958

LinkedHashMap 简介

  • 定义: LinkedHashMap 是根据插入或访问顺序实现有序输出的HashMap
  • 数据结构 : HashMap + 双向链表
  • 使用示例:
//普通HashMap
Map<Integer, String> hashMap = new HashMap<Integer, String>();
hashMap.put(3, "order3");
hashMap.put(1, "order1");
hashMap.put(2, "order2");
hashMap.forEach((key, value) -> System.out.println(key + "-->" + value));

输出结果:
1-->order1
2-->order2
3-->order3

//迭代顺序和插入顺序保持一致
Map<Integer, String> linkedHashMap = new LinkedHashMap<Integer, String>();
linkedHashMap.put(3, "order3");
linkedHashMap.put(1, "order1");
linkedHashMap.put(2, "order2");
linkedHashMap.forEach((key, value) -> System.out.println(key + "-->" + value));

输出结果:
3-->order3
1-->order1
2-->order2

//迭代顺序和查询顺序保持一致
Map<Integer, String> linkedHashMap = new LinkedHashMap<Integer, String>(16, 0.75f, true);
linkedHashMap.put(3, "order3");
linkedHashMap.put(1, "order1");
linkedHashMap.put(2, "order2");

linkedHashMap.get(3);

linkedHashMap.forEach((key, value) -> System.out.println(key + "-->" + value));

输出结果:
1-->order1
2-->order2
3-->order3

// 插入和删除元素后不影响迭代顺序
Map<Integer, String> linkedHashMap = new LinkedHashMap<Integer, String>(16, 0.75f, true);
linkedHashMap.put(3, "order3");
linkedHashMap.put(1, "order1");
linkedHashMap.put(2, "order2");

linkedHashMap.get(3);
linkedHashMap.remove(1);
linkedHashMap.put(4, "order4");

linkedHashMap.forEach((key, value) -> System.out.println(key + "-->" + value));

输出结果:
2-->order2
3-->order3
4-->order4
  • 使用场景: LRU 缓存过期策略 (通过 LinkedHashMap 的访问顺序实现)

LinkedHashMap 实现

  • 首先定义一个双向链表
//扩展一个双向链表的entry
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);
    }
}

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

//链表尾
transient LinkedHashMap.Entry<K,V> tail;
  • 过构造函数掺入accessOrder值控制插入顺序或访问顺序存储
//LinkedHashMap的构造函数大多数默认存储顺序为插入顺序accessOrder=false,通过下面的构造函数可以自由设置accessOrder的值,为true时存储顺序为访问顺序
public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}
  • 初始化时会调用HashMap的初始化方法,并初始化一个空链表
//重写HashMap 的reinitialize方法,初始化一个空链表
void reinitialize() {
    super.reinitialize();
    head = tail = null;
}
  • 如何实现插入顺序存储
//直接复用HashMap的put方法,但是重写了newNode 和 newTreeNode方法
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;
}

TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
    TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
    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;
    }
}

//插入之后如果需要移除最老的节点,LinkedHashMap重写了afterNodeInsertion方法,但是可以看到removeEldestEntry这个方法直接返回的false,因此如果要生效泽应该重写removeEldestEntry该方法
void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}
  • 访问(当accessOrder=true时,通过afterNodeAccess重排数据,把访问节点移动到链表尾部,实现通过访问顺序输出)
//重写HashMap get方法
public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    // 如果 accessOrder 为 true,则调用 afterNodeAccess,将被访问节点移动到链表最后
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

//重写 HashMap afterNodeAccess,将被访问的节点移动到链表最后
void afterNodeAccess(Node<K,V> e) {
    LinkedHashMap.Entry<K,V> last;
    //如果 accessOrder 为 true, 则将被访问节点移动到链表最后
    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)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}
  • LinkedHashMap 重写了afterNodeRemoval方法,删除节点后重新整理链表
void afterNodeRemoval(Node<K,V> e) { // unlink
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    //待删除节点 p 的前置后置节点都置空
    p.before = p.after = null;
    //如果b为空,p就是头节点,删除之后a就是头节点,否则将前置节点b的后置节点指向a
    if (b == null)
        head = a;
    else
        b.after = a;
    //如果a为空,那么p就是尾节点,删除之后,b就是新的尾节点,否则a的前置节点就改为b
    if (a == null)
        tail = b;
    else
        a.before = b;
}
  • LinkedHashMap的迭代其实就是双向链表的迭代
//自定义LinkedHashIterator迭代器
abstract class LinkedHashIterator {
    LinkedHashMap.Entry<K,V> next;
    LinkedHashMap.Entry<K,V> current;
    int expectedModCount;

    LinkedHashIterator() {
        // 保证从链表的头开始扫描
        next = head;
        expectedModCount = modCount;
        current = null;
    }

    public final boolean hasNext() {
        return next != null;
    }

    final LinkedHashMap.Entry<K,V> nextNode() {
        LinkedHashMap.Entry<K,V> e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        current = e;
        next = e.after;
        return e;
    }

    public final void remove() {
        Node<K,V> p = current;
        if (p == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        current = null;
        K key = p.key;
        removeNode(hash(key), key, null, false, false);
        expectedModCount = modCount;
    }
}

问答环节

1、@mackong  问: afterNodeAccess 方法中的 if(accessOrder&&(last=tail)!=e) 判断的作用是?
答: 此处 accessOrder 的判断是私有方法的二次确认判断,放置外部未判断从而导致破坏顺序.

2、@羊大王  问: afterNodeAccess 方法执行之后是否会改变 HashMap 的结构?
答: LinkedHashMap 在 HashMap 的基础上维护了一个双向链表,因此 afterNodeAccess 方法只会调整链表的结构.