【源代码】LinkedHashMap源代码剖析

注:下面源代码基于jdk1.7.0_11

之前的两篇文章通过源代码分析了两种常见的Map集合,HashMap和Hashtable。本文将继续介绍还有一种Map集合——LinkedHashMap。

顾名思义,LinkedHashMap除了是一个HashMap之外。还带有LinkedList的特点。也就是说可以保持遍历的顺序和插入的顺序一致,那么它是怎么做到的呢?以下我们開始分析。
首先看构造器。
public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>

LinkedHashMap直接继承自HashMap,所以拥有HashMap的大部分特性。比方支持null键和值,默认容量为16。装载因子为0.75。非线程安全等等。
可是LinkedHashMap还有非常多个性的地方,以下来看成员变量:
 private transient Entry<K,V> header;//内部双向链表的头结点
    /**
     *代表这个链表的排序方式,true代表依照訪问顺序。false代表依照插入顺序。
     */
 private final boolean accessOrder;

LinkedHashMap比HashMap多了两个成员变量,当中header代表内部双向链表的头结点。后面我们就会发现,LinkedHashMap除了有个桶数组容纳全部Entry之外,另一个双向链表保存全部Entry引用。

历的时候。并非去遍历桶数组,而是直接遍历双向链表,所以LinkedHashMap的遍历时间不受桶容量的限制,这是它和HashMap的重要差别之中的一个。

而这个accessOrder代表的是是否依照訪问顺序,true代表是,默认是插入顺序。所以我们能够将accessOrder置为true来实现LRU算法,这能够用来做缓存。

再看构造器:
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;
    }

构造器首先都会调用父类也就是HashMap的构造器来初始化桶数组,而accessOrder之后会被初始化,除了最后面的一个构造器同意指定accessOrder外。其它构造器都默认将accessOrder置为了false
读者可能非常奇怪,不是还有个header么。这个双向链表为啥不在构造器中初始化呢?这得回到HashMap中查看hashMap的构造器了:
public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
       ... ...
        init();
    }

HashMap构造器最后一步调用了一个init方法,而这个init方法在HashMap中是个空实现,没有不论什么代码。

这事实上就是所谓的“钩子”,详细代码由子类实现,假设子类希望每次构造的时候都去做一些特定的初始化操作,能够选择复写init方法。

我们看到LinkedHashMap中确实复写了init:
 @Override
    void init() {
        header = new Entry<>(-1, null, null, null);//初始化双向链表
        header.before = header.after = header;//不光是双向链表,还是循环链表
    }

在init方法中,果然初始化了双向链表。并且我们还发现,这不光是个双向链表,还是个循环链表。


HashMap内部的Entry类并没有before和after指针,也就是说LinkedHashMap自己重写了一个Entry类
private static class Entry<K,V> extends HashMap.Entry<K,V> {
        // These fields comprise the doubly linked list used for iteration.
        Entry<K,V> before, after;//前驱、后继指针
        Entry(int hash, K key, V value, HashMap.Entry<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(Entry<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();
        }
    }
这里的Entry选择继承父类的Entry类,也就是说LinkedHashMap中的Entry拥有三个指针。除了前驱后继指针外用于双向链表的连接外。另一个next指针用于解决hash冲突(引用链)。
除此之外,Entry新增了几个方法,remove和addbefore用来操作双向链表不用多说。而recordAccess方法比較特殊,这种方法在HashMap中也是空实现,并在put方法中会调用此方法:
 public V put(K key, V value) {//HashMap的put方法
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<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;
            }
        }
      ... ...
    }

此外,在LinkedHashMap的get方法中,也会调用此方法:

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

也就是说,仅仅要涉及到訪问结点。那么就会调用这种方法。

观察该方法的逻辑:假设accessOrder为true,那么会调用addBefore方法将当前Entry放到双向链表的尾部,终于在我们遍历链表的时候就会发现最近最少使用的结点的都集中在链表头部(从最近訪问最少到最近訪问最多的顺序)。这就是LRU。


LinkedHashMap并没有复写put方法,可是却复写了addEntry和createEntry方法,之前分析HashMap的时候我们就知道了。put方法会调用addEntry将键值对挂到桶的某个合适位置,而addEntry又会调用createEntry方法创建一个键值对对象。因而,LinkedHashMap事实上是间接更改了put方法,想想也非常easy理解,LinkedHashMap除了要向桶中加入键值对外,还需向链表中添加键值对,所以必须得改动put方法。
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);
        }
    }
    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++;
    }

createEntry方法会将键值对分别挂到桶数组和双向链表中。
比較有意思的是addEntry方法。它提供了一个可选的操作。我们能够通过继承LinkedHashMap并复写removeEldestEntry方法让该子类能够自己主动地删除近期最少訪问的键值对——这能够用来做缓存!!

LinkedHashMap自己定义了迭代器以及迭代规则。LinkedHashMap是通过内部的双向链表来完毕迭代的。遍历时间与键值对总数成正比,而HashMap遍历时间与容量成正比,所以通常情况下。LinkedHashMap遍历性能是优于HashMap的。可是由于须要额外维护链表。所以折中来看,两者性能相差无几。
 private abstract class LinkedHashIterator<T> implements Iterator<T> {
        Entry<K,V> nextEntry    = header.after;//指向链表首部
        Entry<K,V> lastReturned = null;
        int expectedModCount = modCount;
        public boolean hasNext() {
            return nextEntry != header;
        }
        public void remove() {
            if (lastReturned == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            LinkedHashMap.this.remove(lastReturned.key);
            lastReturned = null;
            expectedModCount = modCount;
        }
        Entry<K,V> nextEntry() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (nextEntry == header)
                throw new NoSuchElementException();
            Entry<K,V> e = lastReturned = nextEntry;
            nextEntry = e.after;
            return e;
        }
    }
总结:
1.LinkedHashMap继承自HashMap。具有HashMap的大部分特性,比方支持null键和值。默认容量为16,装载因子为0.75,非线程安全等等;
2.LinkedHashMap通过设置accessOrder控制遍历顺序是依照插入顺序还是依照訪问顺序。当accessOrder为true时。能够利用其完毕LRU缓存的功能;
3.LinkedHashMap内部维护了一个双向循环链表,而且其迭代操作时通过链表完毕的。而不是去遍历hash表。











posted @ 2016-01-16 17:14  zfyouxi  阅读(185)  评论(0编辑  收藏  举报