数据结构—LinkedHashMap

一、LinkedHashMap的内部数据结构

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

    /**
     * The head of the doubly linked list.
     */
    private transient LinkedHashMapEntry<K,V> header;

    /**
     * The iteration ordering method for this linked hash map: <tt>true</tt>
     * for access-order, <tt>false</tt> 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<K,V> extends HashMapEntry<K,V> {
        // These fields comprise the doubly linked list used for iteration.
        LinkedHashMapEntry<K,V> before, after;

        LinkedHashMapEntry(int hash, K key, V value, HashMapEntry<K,V> 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<K,V> 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<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();
        }
    }

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

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

init()这个方法是在LinkedHashMap父类的构造方法中调用的,这里重写了这个方法。执行完这个init()方法后就有了这样一个header节点:
header节点
这个节点的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<K,V> 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<K,V> 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<K,V> old = table[bucketIndex];
        LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
        table[bucketIndex] = e;
        e.addBefore(header);
        size++;
    }

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

执行过程图
此时old = NULL,接着往下走,假设new LinkedHashMapEntry<>(hash, key, value, old)valueE,则执行完LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);table[bucketIndex] = e;这两行代码后的图为:
添加新元素后的图
真正绕的逻辑来了,当然就是e.addBefore(header);这句代码,再来看看addBefore这个方法的代码:

        private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }

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

执行过程示意图
理解清楚了前一行代码,再来理解after.before = this;这行代码就不是事了,所以执行完这行代码后如图:
执行过程示意图
分析到这里就已经将E节点put进来了,按照刚才分析的逻辑,假如现在再put一个T节点看看又是怎样一个 结构:
又添加一个T节点后的图
嗯,没错,就是这样一幅图,线条比较乱(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<K, V>(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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值