美文网首页
LinkedHashMap 源码解析(基于 JDK1.7)

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

作者: GCYML | 来源:发表于2019-03-19 11:19 被阅读0次

    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 的基础上,加入了一个双向链表,所有的键值对元素都在这条双向链表上。通过维护双向链表,实现了顺序迭代。

    相关文章

      网友评论

          本文标题:LinkedHashMap 源码解析(基于 JDK1.7)

          本文链接:https://www.haomeiwen.com/subject/ghcgmqtx.html