java LinkedHashMap 底层实现和源码分析

开局分析

首先外面来认识下这个类,LinkedHashMap,从继承和实现方面讲,继承自HashMap,且实现了Map接口,且内部未做线程安全处理。那么这里对HashMap不是太熟悉的可以先参考我前面写过的《java HashMap 源码分析和底层实现》,作为一个Map体系的代表性集合,其经典使用场景应该就是LRU-cache的应用了。现在还是按照老龟巨!从源码开始分析

成员及构造方法

成员变量方面

/**
 * 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.
 *
 */
 private final boolean accessOrder;

 private static final long serialVersionUID = 3801124242820219131L;

 在继承了HashMap的非私有成员变量的基础上,新增了3个变量,这里关注header和accessOrder。accessOrder的注释写得很清楚,该布尔变量决定的是遍历时候所使用的顺序,如果是true就采取访问顺序,false的话就是插入顺序来遍历;而header,又难免让我们想起当时分析HashMap里面的table.这里可以点进去看究竟LinkedHashMapEntry<K,V>是个什么类型。

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();
        }
    }

到这里已经可以知道,相对比HashMap,LinkedHashMap内部不光是使用HashMap中的哈希表来存储Entry对象,还另外维护了一个LinkedHashMapEntry,这些LinkedHashMapEntry内部又保存了前驱跟后继的引用,可以确定这是个双向链表。而这个LinkedHashMapEntry提供了对象的增加删除方法都是去更改节点的前驱后继指向。

其实这次我们分析LinkedHashMap要把前面HashMap的代码拿出来讲,可能更直观。现在我们来看构造方法。LinkedHashMapd的构造方法如下:

public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }
public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }
public LinkedHashMap() {
        super();
        accessOrder = false;
    }
public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super(m);
        accessOrder = false;
    }
public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }
@Override
    void init() {
        header = new LinkedHashMapEntry<>(-1, null, null, null);
        header.before = header.after = header;
    }

很好,结合前面的accessOrder成员分析,我们可以得出结论,LinkedHashMap的遍历,默认是插入排序遍历。而其他的则是使用了HashMap构造方法的逻辑。但是不要忘了,HashMap的构造方法实现结构,这里有个init()方法没有被实现。

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY) {
            initialCapacity = MAXIMUM_CAPACITY;
        } else if (initialCapacity < DEFAULT_INITIAL_CAPACITY) {
            initialCapacity = DEFAULT_INITIAL_CAPACITY;
        }

        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        threshold = initialCapacity;
        init();
    }
void init() {
    }

但是到了LinkedHashMap类里面,不难发现这个init()方法已经被重写,这里对header进行了初始化,其hash值为-1,并暂时将header的前驱后继都指向了自己

关注put方法

但是我们找半天没有发现LinkedHashMap类内部并不存在一个put方法的重写,但是他干了一件事,那就是在put方法内部执行到addEntry()方法的时候,对这个方法进行了重写

/**
 * HashMap的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所重写的方法

void addEntry(int hash, K key, V value, int bucketIndex) {
                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);
    }

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++;
    }

不难发现,LinkedHashMap在table指定位置插入Entry的同时,还会把自己所维护的双向链表的元素进行操作。但是现在我们模拟一遍,那就是说,当我创建好一个LinkedHashMap对象的时候,此时header的前驱后继指向的是自己。当我执行put方法添加元素进来的时候,那么此时肯定不会让上面最外层的判断成立并执行if流程内容,那么这个时候就还是走了原来的逻辑,将元素添加到哈希数组table的指定下标。就这么完事了吗?不,LinkedHashMap还重写了createEntry(),那在父类的addEntry()方法内还会走到重写的这个方法里的逻辑。

这里步骤就相当于:

  1. 把hash值,key-value还有对应在哈希table的下标作为参数,先获取完table中该下标位置的Entry对象
  2. 创建一个LinkedHashMapEntry对象,该对象的后继指向原Entry对象,这里的后继并非单项链表的后继,而是LinkedHashMap中所维护的双向链表header的前驱后继关系
  3. 然后在哈希table中指定位置保存新的LinkedHashMapEntry对象。
  4. 最后再修改新添加的LinkedHashMapEntry在双向链表中的位置,即链头。下面分析其操作过程

前面第一次插入的时候第4步骤,已经有经过这样的一套操作:

  1. 将header作为新插入元素的后继
  2. 将header的前驱(第一次插入的时候,header的前驱还是指向自己)作为新元素的前驱
  3. 新元素的前驱(现已变成header)的后继指向新元素
  4. 最后让新元素的后继(现已变成header)指向新元素

如此一来,那么LinkedHashMap的LinkedHashMapEntry就建立了一套前驱后继关系,即元素1-header,header-1,形成一个双向链表;反过来,当我第二次再put进哈希table时候,此时判断header的后继,即链头Entry如果不是header自己的话,说明已经有一个双向链表,且链头Entry是作为了最年长的元素

这里有个很有意思的地方,如果以该链头年长Entry为参,去执行removeEldestEntry()方法,如果返回的是true,那么会执行一个removeEntryForKey的方法!!!

天呐,这里居然有一个这样的流程,等等,我们继续挖掘一下。

在LinkedHashMap内部没有对removeEntryForKey的重写,直接去HashMap类里面看都做了什么操作

final Entry<K,V> removeEntryForKey(Object key) {
        if (size == 0) {
            return null;
        }
        int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
        int i = indexFor(hash, table.length);
        HashMapEntry<K,V> prev = table[i];
        HashMapEntry<K,V> e = prev;

        while (e != null) {
            HashMapEntry<K,V> next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }

哇塞!这里居然会把这个key给删除掉!!!我的天,还有删除的操作,那是不是我用LinkedHashMap的话,保存东西,会在某种条件成里的情况下,把我链头的最年长元素删除?答案是不可能主动出现的,为什么?请看方法

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

我丢你个螺母?我追了一阵子,你就告诉我这个是false?那这样有什么意义你告诉我,你还不如直接不写。但是这里就是一个钩子方法,包括LRU-cache机制,也是借助了这个方法才能实现。

那这里问题来了,我们现在知道,我就算是重新写了个类继承自LinkedHashMap并重写了这个方法,设置成true,那我后面添加元素,还是要被删,而且删除的还是我链头最年长的Entry。请问 ,LinkedHashMap是怎样保证一个一个Entry是否是最年长的呢?是不是我最先put进来的就会是最年长的?毕竟前面代码写的内容,就是把最新添加进来的Entry作为header的前驱,header作为新元素的后继。这里我们需要知道LinkedHashMap控制年长的机制

get方法

前面说完put方法,因为要涉及到LinkedHashMap对双向链表的最年长的控制,这里引入get方法方便理解。来看看get方法的源码

public V get(Object key) {
        LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
        if (e == null)
            return null;
        e.recordAccess(this);
        return e.value;
    }

---------------------------------------------------
/*
 * HashMap#getEntry
 */
final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
        for (HashMapEntry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

我觉得,到这里,估计大家心里就有个底了。在执行完HashMap的get方法之后,这里对get到的元素进行访问记录,在LinkedHashMapEntry的内部的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的accessOrder,是true,那么就是遍历按照访问顺序来的话,就会对双向链表的内容进行排序,也就是把自己在原有的双向链表上面的位置删掉,并在header前面插入。这一下我们就清楚了,为什么我们的LRU-cache可以做到最近最少删除的功能是因为,每次我get一个哈希table里面的元素,就相当于我在双向链表中插入了一个新的元素,这样一来,常被get的元素自然就被不会排到链头,尽管这个元素是最早put进LinkedHashMap的也不怕。而少用到的,自然就会被放置到链头,当我们重写removeEldestEntry方法,自然就可以控制哈希table什么时候去删除内部的元素啦

以上就是我对LinkedHashMap的一番总结,毕竟HashMap的同步问题还有LRU-cache机制的实现这块在我们开发跟面试过程中是常常会碰到的,自己如果不能把来龙去脉搞清楚,心里总是会有些没底气,经过这样一番源码分析,其实自己心里会更有自己的想法跟见解,有什么问题也欢迎大家及时沟通,谢谢

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值