LinkedHashMap 源码解析(基于 JDK1.7)

2019-03-19  本文已影响0人  GCYML

LinkedHashMap 简介

HashMap 是 Java 中非常常见的集合,但是 HashMap 有一个问题,就是迭代HashMap的顺序并不是HashMap放置的顺序。在一些应用场景中,我们是希望获得有序的 Map 的。
正是基于此,就有了 LinkedHashMap。LinkedHashMap 继承了 HashMap,在 HashMap 的基础上,通过内部维护一个双向链表,能够保证元素迭代的顺序。其顺序可以是插入顺序或者是访问顺序。

在这里对源码的分析,就不再重复 HashMap 中已存在的内容,只着重于 LinkedHashMap 中独有的部分。

LinkedHashMap 中新增的属性

LinkedHashMap 中加入了两个属性:

  1. header: 双向链表中的头
  2. accessOrder: 访问顺序,默认为 false,即插入顺序。

LinkedHashMap 内部键值对类 Entry

LinkedHashMap 的内部类 Entry 实现。
Entry 继承了 HashMap 的 Entry,加入了关于双向链表的属性和常用方法。

    private static class Entry<K,V> extends HashMap.Entry<K,V> {
        Entry<K,V> before, after;

        Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
            super(hash, key, value, next);
        }

        // 从双向链表中删除
        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;
        }

        // 若为访问顺序,则移动节点到链尾
        void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            if (lm.accessOrder) {
                lm.modCount++;
                remove();
                addBefore(lm.header);
            }
        }

        // 删除
        void recordRemoval(HashMap<K,V> m) {
            remove();
        }
    }

LinkedHashMap 重写的方法

构造方法

构造方法调用了 HashMap 的方法,加入了初始化 accessOrder 为 false。以及重写了 init 方法。

init 方法是在 HashMap 构造方法中会调用到的方法。原来为空实现,这里加入了初始化双向链表的操作。

    @Override
    void init() {
        header = new Entry<>(-1, null, null, null);
        header.before = header.after = header;
    }

添加元素时对双向链表的维护

addEntry 方法

    void addEntry(int hash, K key, V value, int bucketIndex) {
        super.addEntry(hash, key, value, bucketIndex);

        // Remove eldest entry if instructed
        Entry<K,V> eldest = header.after;
        if (removeEldestEntry(eldest)) {
            removeEntryForKey(eldest.key);
        }
    }

createEntry 方法

创建键值对放入桶中,在双向链表链尾中加入该节点。可以看到这里所有的插入操作,都是把元素插入到链表的头部,因此只要遍历 after,即为插入排序迭代。

    void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMap.Entry<K,V> old = table[bucketIndex];
        Entry<K,V> e = new Entry<>(hash, key, value, old);
        table[bucketIndex] = e;
        e.addBefore(header);
        size++;
    }

transfer 方法

transfer 方法只有 resize 方法会调用到,也就是 Map 动态扩充时。
这里就是遍历双向链表,用头插法把键值对插入到桶中。

    @Override
    void transfer(HashMap.Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e = header.after; e != header; e = e.after) {
            if (rehash)
                e.hash = (e.key == null) ? 0 : hash(e.key);
            int index = indexFor(e.hash, newCapacity);
            e.next = newTable[index];
            newTable[index] = e;
        }
    }

访问顺序的实现原理

在调用 get 方法和添加元素时键值已存在时,均会调用 Entry 的 recordAccess 方法。可以看到,若为访问顺序时, recordAccess 会把该元素移动到链表的头部。

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

基于 LinkedHashMap 实现 FIFO 和 LRU

FIFO(First In First out): 先见先出,淘汰最先近来的页面,新进来的页面最迟被淘汰,完全符合队列。
LRU(Least recently used): 最近最少使用,淘汰最近不使用的页面。

在前面的 addEntry 方法中,这里还调用了一个判断方法 removeEldestEntry,若为真,则删除链尾元素。我们可以利用此方法来实现 FIFOLRU
重写 removeEldestEntry 方法,若超过长度则删除链尾元素。
当为访问顺序时,由于长访问的都被移动到了链头位置,因此最少访问的元素在链表尾部。所以实现了 LRU
为插入顺序时,最早插入的元素被移动到了链尾位置,删除链尾元素,则实现了 FIFO

    void addEntry(int hash, K key, V value, int bucketIndex) {
        super.addEntry(hash, key, value, bucketIndex);

        // Remove eldest entry if instructed
        Entry<K,V> eldest = header.after;
        if (removeEldestEntry(eldest)) {
            removeEntryForKey(eldest.key);
        }
    }

总结

LinkedHashMap 是在 HashMap 的基础上,加入了一个双向链表,所有的键值对元素都在这条双向链表上。通过维护双向链表,实现了顺序迭代。

上一篇下一篇

猜你喜欢

热点阅读