数据结构—LinkedHashMap

原文链接: https://my.oschina.net/devbird/blog/829759

一、LinkedHashMap的内部数据结构

其实分析一个数据结构主要是分析清楚数据之间的关系,理清出了数据的存储、删除、读取等关系,数据的结构也就清楚了。LinkedHashMap内部也有两个很重要的成员变量:

    /**
     * The head of the doubly linked list.
     */
    private transient LinkedHashMapEntry header;

    /**
     * The iteration ordering method for this linked hash map: true
     * for access-order, false for insertion-order.
     *
     * @serial
     */
    private final boolean accessOrder;

其中accessOrder如果为true时,表示使用访问排序;为false时表示使用插入排序,而默认是为false的。如果要实现访问排序就需要用到LinkedHashMap的这个构造函数:

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

在构造的过程中accessOrder这个参数传入true即可。再来看看header这个成员变量的数据类型的代码:

    private static class LinkedHashMapEntry extends HashMapEntry {
        // These fields comprise the doubly linked list used for iteration.
        LinkedHashMapEntry before, after;

        LinkedHashMapEntry(int hash, K key, V value, HashMapEntry next) {
            super(hash, key, value, next);
        }

        /**
         * Removes this entry from the linked list.
         */
        private void remove() {
            before.after = after;
            after.before = before;
        }

        /**
         * Inserts this entry before the specified existing entry in the list.
         */
        private void addBefore(LinkedHashMapEntry existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }

        /**
         * This method is invoked by the superclass whenever the value
         * of a pre-existing entry is read by Map.get or modified by Map.set.
         * If the enclosing Map is access-ordered, it moves the entry
         * to the end of the list; otherwise, it does nothing.
         */
        void recordAccess(HashMap m) {
            LinkedHashMap lm = (LinkedHashMap)m;
            if (lm.accessOrder) {
                lm.modCount++;
                remove();
                addBefore(lm.header);
            }
        }

        void recordRemoval(HashMap m) {
            remove();
        }
    }

可以看出LinkedHashMapEntryHashMapEntry的子类,但是它而外添加了beforeafter两个成员变量用于指向当前节点的前一个节点和后一个节点,形成双链链表。到现在依然还没有理清楚LinkedHashMap内部数据存储的关系,下面再一步一步来分析,先来看看init()这个方法:

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

init()这个方法是在LinkedHashMap父类的构造方法中调用的,这里重写了这个方法。执行完这个init()方法后就有了这样一个header节点:
数据结构—LinkedHashMap_第1张图片
这个节点的before指针和after指针都指向了自己,构成了一个简单的双链回环链表。既然要分析清楚数据间的关系,所以从添加元素的方法开始分析,看内部的数据间的关系是如何变化的,就能一目了然了。但是LinkedHashMapEntry里面并没有put()方法,那肯定就是使用的父类的,所以再来看看父类的这个put()方法:

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
        int i = indexFor(hash, table.length);
        for (HashMapEntry e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

其实LinkedHashMap是通过重写recordAccessaddEntry这两个方法来实现自己特定需求的,而重写recordAccess这个方法主要是用来实现访问排序的,所以这里就暂时不分析,来看看addEntry的代码:

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

        // Remove eldest entry if instructed
        LinkedHashMapEntry eldest = header.after;
        if (eldest != header) {
            boolean removeEldest;
            size++;
            try {
                removeEldest = removeEldestEntry(eldest);
            } finally {
                size--;
            }
            if (removeEldest) {
                removeEntryForKey(eldest.key);
            }
        }

        super.addEntry(hash, key, value, bucketIndex);
    }

这个方法中前面干的那些事都是为了删除最老的元素,后面又调用了父类的addEntry方法,所以又得再来看看父类的addEntry方法:

    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

这个方法的前面部分把该扩容的事情做了后,调用了 createEntry方法,但是在这个方法里面调用的createEntry这方法又是被子类LinkedHashMap重写了的,所以主要实现又来到了子类的createEntry这个方法:

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

来来回回的子类父类相互调用是不是有点晕了呢?现在就通过画图一步一步来分析这里面的每句代码,假设是第一次调用put方法而且bucketIndex=2,执行完HashMapEntry old = table[bucketIndex];这句代码后就可以得到一幅图:

数据结构—LinkedHashMap_第2张图片
此时old = NULL,接着往下走,假设new LinkedHashMapEntry<>(hash, key, value, old)valueE,则执行完LinkedHashMapEntry e = new LinkedHashMapEntry<>(hash, key, value, old);table[bucketIndex] = e;这两行代码后的图为:
数据结构—LinkedHashMap_第3张图片
真正绕的逻辑来了,当然就是e.addBefore(header);这句代码,再来看看addBefore这个方法的代码:

        private void addBefore(LinkedHashMapEntry existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }

现在来分析分析e.addBefore(header);这行代码,调用这个方法的是e这个节点,所以addBefore这个方法中的afterbefore这个两个指针当然是e节点中的成员变量。现在用H表示传入的header这个变量,当执行完 after = existingEntry;这行代码后如图: 数据结构—LinkedHashMap_第4张图片
因为existingEntry就是传进来的header,而在前面我们画了一个header的示意图,从header的那个图中可以看出header.before就是指向自己的,所以执行完before = existingEntry.before;这行代码后就是下面这幅图的样子了:
数据结构—LinkedHashMap_第5张图片
再来理解哈 before.after = this;这一行代码,因为是e这个节点调用的addBefore这个方法,所以before就是e这个节点中的变量。但是before这个指针已经指向了header,所以before.after就是指header中的after这个指针,另外this肯定就是指`e这个节点,当执行完这行代码后如图:

数据结构—LinkedHashMap_第6张图片
理解清楚了前一行代码,再来理解after.before = this;这行代码就不是事了,所以执行完这行代码后如图:
数据结构—LinkedHashMap_第7张图片
分析到这里就已经将E节点put进来了,按照刚才分析的逻辑,假如现在再put一个T节点看看又是怎样一个 结构:
数据结构—LinkedHashMap_第8张图片
嗯,没错,就是这样一幅图,线条比较乱(3组线,6条线),但是对着代码来看还是很清晰的。另外需要说明的一点是,其实每个节点之间还有一个next指针的,为了简化就没有在这些过程中画出来,自己心里能明白这一点就好。 看到这里相信对LinkedHashMap的内部数据结构也有了一个比较清晰的认识了,其是就是在HashMap的基础上让节点之间实现了一个双向回环链表!


二、LinkedHashMap在LruCache中的使用

前面通过详细的分析LinkedHashMapput方法,也对LinkedHashMap有了更深刻的认识,相信再来分析getremove方法就不在话下了。现在来看看它的排序算法,LinkedHashMap中实现了两种排序算法,分别是插入排序和访问排序。插入排序很好理解,就是按照存放元素的顺序来排序;而访问排序就是将最近访问的元素移到最前面,而最不常访问的就自然排到了最后面,当想要删除最不常访问的元素时直接干掉最后面的就OK了,所以要实现LRU(Least Recently Used )算法很容易了。Android中经常用到的LruCache算法就是基于LinkedHashMap实现的,LruCache的构造函数代码如下:

    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap(0, 0.75f, true);
    }

从这这个构造函数可以看出在实例化LinkedHashMap的时候,第三个参数传的true,表示要用到LinkedHashMap的访问排序算法,在LruCache中也没有特别需要分析的方法了,我觉得只要会用LruCache并且知道它是用了LinkedHashMap来实现的LRU算法即可,因为明白了LinkedHashMap也就明白了LruCache实现的核心。


现在来总结下LinkedHashMap的几个重要的特点:
LinkedHashMap也支持keyvaluenull的情况;
LinkedHashMap是在HashMap的基础上实现了双链回环链表的一种数据结构;
LinkedHashMap存放的元素是可以支持有序的; ④LinkedHashMap内有两种排序算法,一种是基于插入排序;另一种是基于访问排序。
我觉得最需要记住的是第四个特点,在开发的过程中遇到需要类似排序算法的是否要能想到使用LinkedHashMap,这也是分析LinkedHashMap内部实现原理的重要原因。

转载于:https://my.oschina.net/devbird/blog/829759

你可能感兴趣的:(数据结构—LinkedHashMap)