LinkedHashMap工作原理

大部分翻译自:LinkedHashMap

在这篇文章中,我们将会一探LinkedHashMap的内部工作原理。

LinkedHashMap VS HashMap

LinkedHashMap 也是一个HashMap,但它使用额外的数据结构——双向链表,定义了遍历顺序。默认情况下,遍历顺序等同于插入顺序。它也可以是entries最近被访问的顺序,所以它也能很容易被用来当做LRU Cache。在oscache或者是ehcache都使用到了LinkedHashMap。(oscache中是否使用LRU是可以配置的)

数据结构

LinkedHashMap的数据结构拓展自HashMap

HashMap是基于数组和链表的数据结构。一个entry基于它的hash值确定它在数组的下标位置。如果数组下标位置已经存在元素了,HashMap使用链表头插入法(jdk1.7下),新的entry会插入链表的头部,并将next引用指向旧的entry。

在HashMap中,无法控制迭代顺序(英文术语:iteration order)。

在LinkedHashMap中,迭代顺序是被定义的,要么是插入顺序,要么是访问顺序。

LinkedHashMap区别于HashMap的最大特点是,所有的entry构成了一个双向链表结构。

下图是LinkedHashMap的数据结构的示例,它基于插入顺序定义迭代顺序。为了实现这一点,每个entry元素都会跟踪它的前一个元素和后一个元素。如果LinkedHashmap的大小为0,那么它只包含一个head元素,head元素的before引用和after引用都会指向head本身。

Entry

LinkedHashMap的entry拓展了HashMap的Entry,所以它也继承了同样的属性:key、value、hash、next Entry。除此之外,它有一对额外的属性取维护这个双向链表结构,after entry 和 before entry 。

New Entry 

LinkedHashMap继承了Hashmap,所以它的内部数据结构是几乎与HashMap相同的。不同的是,它维护了一个双向链表,而这个双向链表循环链接的实现,是通过head元素这个哨兵节点实现的。每个节点都保持了前一个节点和后一个节点的引用。一个新的节点总是会添加到这个双向链表的末端(不管是基于插入顺序还是访问顺序,都是如此)。为了实现末端插入新节点,末端节点和head节点之间的链接必须要做调整。

1、新节点的after引用指向head节点;

2、新节点的before引用指向当前的末端节点;

3、当前末端节点的after引用指向新节点;

4、head节点的before引用指向新节点;

after  = head;
before = head.before;
before.after = this;
after.before = this;
ps:双向链表使用head这个哨兵节点的意义:上面实现末端插入,只需要知道head节点的引用就可以了,通过head节点可以求出:

当前末端节点:head.before

当前末端节点的after引用:head.before.after

head节点的before引用:head.before

在LinkedHashMap的内部静态类Entry中,addBefore()方法对应了把新的节点添加到双向链表的末端的实现。源码如下:

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

        Entry(int hash, K key, V value, HashMap.Entry 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(Entry 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);
            }
        }
   ....................
}

LinkedHashMap没有对父类HashMap的put(key,vlaue) 方法进行任何直接的修改,它只对put()方法中的addEntry方法和Entry的recordAccess方法进行了重写。基于这一点,可以把它看做模板模式的实现。

     * This override alters behavior of superclass put method. It causes newly
     * allocated entry to get inserted at the end of the linked list and
     * removes the eldest entry if appropriate.
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {
        createEntry(hash, key, value, bucketIndex);   //addEntry底层核心是调用createEntry()方法

        // Remove eldest entry if instructed, else grow capacity if appropriate
        Entry eldest = header.after;
        if (removeEldestEntry(eldest)) {
            removeEntryForKey(eldest.key);
        } else {
            if (size >= threshold)
                resize(2 * table.length);
        }
    }

    /**
     * This override differs from addEntry in that it doesn't resize the
     * table or remove the eldest entry.
     */
    void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMap.Entry old = table[bucketIndex];
        Entry e = new Entry(hash, key, value, old);//旧的bucketIndex的entry作为参数传给新的entry的next引用
        table[bucketIndex] = e;
        e.addBefore(header);  //createEntry()方法调用addBefore将新节点添加到双向链表的末端
        size++;
    }

Access Ordered

LinkedHashMap(capacity, loadFactor, accessOrderBoolean)构造函数可以用来创建通过访问顺序进行迭代的hashmap,从最近最少访问到最近最多访问。调用put或者get方法就会形成对相应entry的一次访问,在判断LinkedHashMap是通过访问顺序进行迭代后,它会把这个被访问的entry移到双向链表的末端,否则它什么也不做。

public void testLinkedHashMap() {
    LinkedHashMap lru = new LinkedHashMap(16, 0.75f, true);
    lru.put("one", null);
    lru.put("two", null);
    lru.put("three", null);
 
    Iterator itr = lru.keySet().iterator();
    while (itr.hasNext()) {
        System.out.println(itr.next());
    }
 
    System.out.println("** Access one, will move it to end **");
    lru.get("one");
 
    itr = lru.keySet().iterator();
    while (itr.hasNext()) {
        System.out.println(itr.next());
    }
 
    System.out.println("** Access two, will move it to end **");
    lru.put("two", "two");
 
    itr = lru.keySet().iterator();
    while (itr.hasNext()) {
        System.out.println(itr.next());
    }
}
Result:
one
two
three
** Access one, will move it to end **
two
three
one
** Access two, will move it to end **
three
one
two

通过上面示例可以看出,在这个基于访问顺序的LinkedHashMap中,调用get()方法查询map就会引起其内部结构的修改。

备注: In access-ordered linked hash maps, merely querying the map with get is a structural modification. 是官网API中重点划出的句子。但从上面的示例可以看出,调用put()方法存在相同的key覆盖旧的value,基于访问顺序下也会引起结构的修改和迭代顺序的改变。

按照访问的次序来排序的含义:当调用LinkedHashMap的get(key)或者put(key, value)时,如果在map中包含这个key,那么LinkedHashMap会调用Entry的的recordAccess(HashMap)方法,将key对象的entry放在双向链表的末端。

ps:也就是说,当使用put()方法,存在相同的key覆盖旧的value后,也会把该key对应的entry放到双向链表的末端。而如果不存在这个key,按照原先的约定,就是将新的entry放到双向链表的末端。

正是因为LinkedHashMap提供按照访问的次序来排序的功能,所以它才需要改写HashMap的get(key)方法(HashMap不需要排序)和HashMap.Entry的recordAccess(HashMap)方法
public Object get(Object key) {
        Entry e = (Entry)getEntry(key);
        if (e == null)
            return null;
       e.recordAccess(this);
        return e.value;
    }


void recordAccess(HashMap m) {
            LinkedHashMap lm = (LinkedHashMap)m;
            if (lm.accessOrder) {
                lm.modCount++;
                remove();   //在双向链表上删除该节点
                addBefore(lm.header);  //将该节点添加到双向链表上head节点的前面,即放在双向链表的末端
            }
        }

至于put(key, value)方法, LinkedHashMap不需要去改写,用HashMap的就可以了,因为HashMap在其put(key, value)方法里边已经预留了e.recordAccess(this);  HashMap的put()源码如下:

 public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry 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;
    }


Iterator

在HashMap中,迭代器必须遍历每个table元素和元素上的链表,遍历的时间复杂度与HashMap的capacity成正比。

在LinkedHashMap中,迭代器只需要遍历双向链表,因此时间复杂度只与map的大小有关,与capacity无关。所以HashMap迭代的开销是更大的。

Transfer

LinkedHashMap的resize扩容操作是更快的,因为它只需要遍历双向链表,就可以把内容transfer到新的table array上。

void transfer(HashMap.Entry[] newTable) {
    int newCapacity = newTable.length;
    for (Entry e = header.after; e != header; e = e.after) {
        int index = indexFor(e.hash, newCapacity);
        e.next = newTable[index];
        newTable[index]= e;
    }
}

在HashMap的transfer操作,则需要两层循环,先遍历table,在遍历每个table元素上的链表。

contains  value

containsValue()方法被重写,从而充分利用迭代器更快的性能。

LRU Cache

如果基于访问顺序,迭代器的迭代顺序是基于entry被最近访问的顺序,也就是从最近最少访问到最近最多访问的访问顺序。这个类型的map非常适用于构建LRU Cache。removeEldestEntry(Entry)可以被重写,从而实现当一个新的entry添加到map中时自动删除过时的entry的策略。例如:

protected boolean removeEldestEntry(Map.Entry eldest) {
    return size() > maxCacheSize;
}

最旧的entry可以通过header.after返回,而默认的removeEldestEntry()的实现是返回false。

======================================

参考:oracle官网 LinkedHashMap API
彻头彻尾理解 LinkedHashMap

LinkedHashMap特性 按插入和访问顺序排序

你可能感兴趣的:(Java)