大话系列 - LinkedHashMap源码分析
再来看一下LinkedHashMap的源码分析,这个数据结构可以方便我们构建LFU时使用,因为其实内部的数据存储是利用了HashMap的功能,然后额外通过一个链表来记录元素之间的顺序关系。
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {
首先看类的继承关系,可以看到是直接继承了HashMap,而且本身也没有实现增删改查的相关操作,实际的数据操作都是直接使用父类的
static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } }
这里定义了一个Entry的子类,用于在LinkedHashMap中记录元素相关顺序,可以看到这个新的数据结构除了Entry基本的属性信息外,只提供了一个前置和后置指针。
/** * The head (eldest) of the doubly linked list. */ transient LinkedHashMap.Entry<K,V> head; /** * The tail (youngest) of the doubly linked list. */ transient LinkedHashMap.Entry<K,V> tail; /** * The iteration ordering method for this linked hash map: <tt>true</tt> * for access-order, <tt>false</tt> for insertion-order. * * @serial */ final boolean accessOrder;
然后看三个成员属性,首先是accessOrder,通过注释我们可以知道这个属性是用来确定元素的迭代排序的方式 ,true表示按照访问顺序,false表示代表元素的插入顺序。那对应的head和tail就是这个列表的头尾指针。
/** * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance * with the specified initial capacity and load factor. * * @param initialCapacity the initial capacity * @param loadFactor the load factor * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive */ public LinkedHashMap(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor); accessOrder = false; } /** * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance * with the specified initial capacity and a default load factor (0.75). * * @param initialCapacity the initial capacity * @throws IllegalArgumentException if the initial capacity is negative */ public LinkedHashMap(int initialCapacity) { super(initialCapacity); accessOrder = false; } /** * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance * with the default initial capacity (16) and load factor (0.75). */ public LinkedHashMap() { super(); accessOrder = false; } /** * Constructs an insertion-ordered <tt>LinkedHashMap</tt> instance with * the same mappings as the specified map. The <tt>LinkedHashMap</tt> * instance is created with a default load factor (0.75) and an initial * capacity sufficient to hold the mappings in the specified map. * * @param m the map whose mappings are to be placed in this map * @throws NullPointerException if the specified map is null */ public LinkedHashMap(Map<? extends K, ? extends V> m) { super(); accessOrder = false; putMapEntries(m, false); } /** * Constructs an empty <tt>LinkedHashMap</tt> instance with the * specified initial capacity, load factor and ordering mode. * * @param initialCapacity the initial capacity * @param loadFactor the load factor * @param accessOrder the ordering mode - <tt>true</tt> for * access-order, <tt>false</tt> for insertion-order * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive */ public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); this.accessOrder = accessOrder; }
构造方法一共提供了5个,但是具体的构造都是以来父类。还有就是如果在构造方法中如果没有明确指定accessOrder的话,这个值是false,那也就是说默认情况下遍历迭代的顺序是按照元素的插入顺序的。
void afterNodeAccess(Node<K,V> e) { // move node to last LinkedHashMap.Entry<K,V> last; if (accessOrder && (last = tail) != e) { LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; p.after = null; if (b == null) head = a; else b.after = a; if (a != null) a.before = b; else last = b; if (last == null) head = p; else { p.before = last; last.after = p; } tail = p; ++modCount; } }
从这里开始咱们看几个子类重写的方法,首先是 afterNodeAccess ,这个方法的调用入口是在HashMap的put方法中,如果能够在原有结构中找到待添加的元素时,会调用这个方法,被访问的Entry节点作为方法入参,但是这个方法在HashMap中是没有实现的。
首先说一下这链表是一个双向链表,如果是按照插入顺序,那么最早添加的元素是在队首,如果是按照访问顺序,那最近最少被访问的元素也是在队首。所以这里会先判断当前需要被处理的元素,如果后指针为null ,那就证明他已经是在队尾了,不需要再调整了。
void afterNodeInsertion(boolean evict) { // possibly remove eldest LinkedHashMap.Entry<K,V> first; if (evict && (first = head) != null && removeEldestEntry(first)) { K key = first.key; removeNode(hash(key), key, null, false, true); } }
接下来就是 afterNodeInsertion 方法,这个也是在put方法中调用的,只是这里是在元素第一次被添加的时候调用的,这里存在一个入参,就是evict属性,是否会驱逐当前Map中的元素,那就必须要看一下 removeEldestEntry 方法了。
/** * Returns <tt>true</tt> if this map should remove its eldest entry. * This method is invoked by <tt>put</tt> and <tt>putAll</tt> after * inserting a new entry into the map. It provides the implementor * with the opportunity to remove the eldest entry each time a new one * is added. This is useful if the map represents a cache: it allows * the map to reduce memory consumption by deleting stale entries. * * <p>Sample use: this override will allow the map to grow up to 100 * entries and then delete the eldest entry each time a new entry is * added, maintaining a steady state of 100 entries. * <pre> * private static final int MAX_ENTRIES = 100; * * protected boolean removeEldestEntry(Map.Entry eldest) { * return size() > MAX_ENTRIES; * } * </pre> * * <p>This method typically does not modify the map in any way, * instead allowing the map to modify itself as directed by its * return value. It <i>is</i> permitted for this method to modify * the map directly, but if it does so, it <i>must</i> return * <tt>false</tt> (indicating that the map should not attempt any * further modification). The effects of returning <tt>true</tt> * after modifying the map from within this method are unspecified. * * <p>This implementation merely returns <tt>false</tt> (so that this * map acts like a normal map - the eldest element is never removed). * * @param eldest The least recently inserted entry in the map, or if * this is an access-ordered map, the least recently accessed * entry. This is the entry that will be removed it this * method returns <tt>true</tt>. If the map was empty prior * to the <tt>put</tt> or <tt>putAll</tt> invocation resulting * in this invocation, this will be the entry that was just * inserted; in other words, if the map contains a single * entry, the eldest entry is also the newest. * @return <tt>true</tt> if the eldest entry should be removed * from the map; <tt>false</tt> if it should be retained. */ protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { return false; }
这里的注释比较长,挑干的说几点:
- 首先这是一个protected的方法,可以被子类覆盖,默认这里是不会删除Map对象中的元素的
- 我们知道HashMap即使我们指定了容量,但是如果添加元素超过了初始化容量,是会出发扩容操作的。但是这方法是提供了一个条件入口,用于自定义删除元素的条件
所以如果我们通过这个对象作为构建缓存的基础数据结构,几个一定要重写这个方法。
再结合上面的双向链表存储结构,那我们可以知道这个Eldest Element如果是按照插入顺序,那就是队尾的那一个元素,如果是按照访问顺序,那就是队首的那一个元素。
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); else if (node == p) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }
这个方法就是删除元素的核心方法了,就是如果满足被驱逐条件的情况下,需要通过这个方法来驱逐相应的元素。上面已经分析过了,如果是按照访问顺序来处理,此时head对象就是需要被驱逐的对象,细心的可以发现其实这个方法就是HashMap中的删除方法。
void afterNodeRemoval(Node<K,V> e) { // unlink LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; p.before = p.after = null; if (b == null) head = a; else b.after = a; if (a == null) tail = b; else a.before = b; }
最后就是这个删除时回调的方法了,其实这里并没有做过多额外的处理,只是上面的删除方法是在table中删除对应对应,但是在双向链表中的对象还没有被删除,那就是在这里进行链表遍历并删除元素。
说到这里不知道大家有没有发现一个问题,就是我们说了,数组的操作是依赖于HashMap提供的能力,然后元素关系是通过链表来维护,而且在插入和删除的关键性方法中都提供了回调方法,用于维护双向链表的结构。那么双向链表是在哪里添加的呢?
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) { LinkedHashMap.Entry<K,V> last = tail; tail = p; if (last == null) head = p; else { p.before = last; last.after = p; } }
这里我们找到了一个核心方法,在创建单链表或者红黑树的时候添加新节点中,都会间接的调用到这个方法,在这里会把新添加的元素添加到链表的尾部,这里不区分是按照什么顺序访问的,新添加的元素也可以理解为最近被访问的元素嘛,与accessOrder也不冲突。

浙公网安备 33010602011771号